From a9577c575670680ef94081c1c3466028a5a196ab Mon Sep 17 00:00:00 2001 From: dungtt Date: Wed, 15 Oct 2025 15:15:53 +0700 Subject: [PATCH] first commit -push --- .env | 19 + .gitignore | 409 +++++ RobotNet.AppHost/Dockerfile | 0 RobotNet.AppHost/Program.cs | 48 + .../Properties/launchSettings.json | 31 + RobotNet.AppHost/RobotNet.AppHost.csproj | 27 + RobotNet.AppHost/appsettings.Development.json | 8 + RobotNet.AppHost/appsettings.json | 9 + RobotNet.Clients/HttpClientExtensions.cs | 36 + RobotNet.Clients/HubClient.cs | 60 + RobotNet.Clients/RobotNet.Clients.csproj | 13 + ...omponentsEndpointRouteBuilderExtensions.cs | 29 + .../Account/IdentityNoOpEmailSender.cs | 21 + .../Account/IdentityRedirectManager.cs | 59 + ...RevalidatingAuthenticationStateProvider.cs | 48 + .../Account/IdentityUserAccessor.cs | 20 + .../Account/Pages/AccessLogin.razor | 35 + .../Components/Account/Pages/Infor.razor | 355 +++++ .../Components/Account/Pages/Infor.razor.css | 7 + .../Components/Account/Pages/Login.razor | 118 ++ .../Account/Pages/LogoutConfirm.razor | 37 + .../Account/Pages/OpenIdDictApplication.razor | 982 ++++++++++++ .../Pages/OpenIdDictApplication.razor.css | 66 + .../Account/Pages/OpenIdDictManager.razor | 21 + .../Account/Pages/OpenIdDictScope.razor | 734 +++++++++ .../Account/Pages/OpenIdDictScope.razor.css | 142 ++ .../Components/Account/Pages/Password.razor | 285 ++++ .../Account/Pages/Password.razor.css | 566 +++++++ .../Components/Account/Pages/Register.razor | 120 ++ .../Components/Account/Pages/Role.razor | 923 +++++++++++ .../Components/Account/Pages/Role.razor.css | 87 ++ .../Account/Pages/UserManager.razor | 17 + .../Account/Pages/UserManager.razor.css | 73 + .../Components/Account/Pages/_Imports.razor | 2 + .../Account/Shared/RedirectToLogin.razor | 8 + RobotNet.IdentityServer/Components/App.razor | 30 + .../Components/Layout/MainLayout.razor | 29 + .../Components/Layout/MainLayout.razor.css | 123 ++ .../Components/Layout/NavMenu.razor | 214 +++ .../Components/Layout/NavMenu.razor.css | 202 +++ .../Components/Pages/Error.razor | 36 + .../Components/Pages/Home.razor | 27 + .../Components/Routes.razor | 12 + .../Components/_Imports.razor | 14 + .../Controllers/AuthorizationController.cs | 402 +++++ .../IdentityServerLoggerController.cs | 48 + .../Controllers/UserinfoController.cs | 63 + .../Data/ApplicationDbContext.cs | 13 + .../Data/ApplicationDbExtensions.cs | 190 +++ .../Data/ApplicationRole.cs | 9 + .../Data/ApplicationUser.cs | 13 + ...085859_InitializeApplicationDb.Designer.cs | 540 +++++++ .../20250716085859_InitializeApplicationDb.cs | 378 +++++ .../ApplicationDbContextModelSnapshot.cs | 537 +++++++ RobotNet.IdentityServer/Dockerfile | 56 + .../Helpers/AsyncEnumerableExtensions.cs | 21 + .../Helpers/FormValueRequiredAttribute.cs | 32 + RobotNet.IdentityServer/Program.cs | 196 +++ .../Properties/launchSettings.json | 15 + .../Properties/serviceDependencies.json | 8 + .../Properties/serviceDependencies.local.json | 8 + .../RobotNet.IdentityServer.csproj | 41 + .../Services/IdentityService.cs | 69 + .../Services/PasswordStrengthService.cs | 61 + .../Services/UserImageService.cs | 26 + .../Services/UserInfoService.cs | 41 + RobotNet.IdentityServer/appsettings.json | 18 + RobotNet.IdentityServer/libman.json | 15 + RobotNet.IdentityServer/nlog.config | 25 + RobotNet.IdentityServer/wwwroot/app.css | 60 + RobotNet.IdentityServer/wwwroot/favicon.svg | 5 + ...aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 | Bin 0 -> 11840 bytes ...n66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2 | Bin 0 -> 20612 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 | Bin 0 -> 9644 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2 | Bin 0 -> 3676 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2 | Bin 0 -> 16848 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 | Bin 0 -> 13740 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 | Bin 0 -> 7856 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2 | Bin 0 -> 10576 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 | Bin 0 -> 19660 bytes .../wwwroot/mud/fonts.googleapis.com.css | 81 + .../wwwroot/uploads/avatars/anh.jpg | Bin 0 -> 302127 bytes .../Controllers/ActionsController.cs | 130 ++ .../Controllers/EdgesController.cs | 269 ++++ .../Controllers/ElementModelsController.cs | 258 ++++ .../Controllers/ElementsController.cs | 211 +++ .../Controllers/ImagesController.cs | 66 + .../MapDesignerLoggerController.cs | 48 + .../Controllers/MapExportController.cs | 249 +++ .../Controllers/MapsDataController.cs | 381 +++++ .../Controllers/MapsManagerController.cs | 571 +++++++ .../Controllers/MapsSettingController.cs | 79 + .../Controllers/NodesController.cs | 58 + .../Controllers/ScriptElementsController.cs | 232 +++ .../Controllers/ZonesController.cs | 114 ++ RobotNet.MapManager/Data/Action.cs | 30 + RobotNet.MapManager/Data/Edge.cs | 77 + RobotNet.MapManager/Data/Element.cs | 49 + RobotNet.MapManager/Data/ElementModel.cs | 55 + RobotNet.MapManager/Data/Map.cs | 125 ++ .../Data/MapEditorDbContext.cs | 90 ++ .../Data/MapManagerDbExtensions.cs | 17 + .../20250425030649_AddMapdb.Designer.cs | 603 ++++++++ .../Migrations/20250425030649_AddMapdb.cs | 313 ++++ .../20250812041834_AddZoneName.Designer.cs | 608 ++++++++ .../Migrations/20250812041834_AddZoneName.cs | 29 + .../MapEditorDbContextModelSnapshot.cs | 605 ++++++++ RobotNet.MapManager/Data/Node.cs | 51 + RobotNet.MapManager/Data/Zone.cs | 61 + RobotNet.MapManager/Dockerfile | 65 + RobotNet.MapManager/Hubs/MapHub.cs | 165 ++ RobotNet.MapManager/Program.cs | 94 ++ .../Properties/launchSettings.json | 15 + .../RobotNet.MapManager.csproj | 29 + RobotNet.MapManager/RobotNet.MapManager.http | 6 + .../Services/LoggerController.cs | 108 ++ .../Services/MapEditorStorageRepository.cs | 140 ++ RobotNet.MapManager/Services/ServerHelper.cs | 161 ++ RobotNet.MapManager/appsettings.json | 31 + RobotNet.MapManager/nlog.config | 24 + RobotNet.MapShares/Dtos/ActionDto.cs | 31 + RobotNet.MapShares/Dtos/EdgeDto.cs | 86 ++ RobotNet.MapShares/Dtos/ElementDto.cs | 49 + RobotNet.MapShares/Dtos/ElementModelDto.cs | 31 + RobotNet.MapShares/Dtos/MapDataExportDto.cs | 66 + RobotNet.MapShares/Dtos/MapDto.cs | 97 ++ RobotNet.MapShares/Dtos/NodeDto.cs | 34 + RobotNet.MapShares/Dtos/ZoneDto.cs | 22 + RobotNet.MapShares/Enums/AlignState.cs | 13 + RobotNet.MapShares/Enums/BlockingType.cs | 7 + RobotNet.MapShares/Enums/ControlState.cs | 16 + RobotNet.MapShares/Enums/Direction.cs | 9 + RobotNet.MapShares/Enums/DirectionAllowed.cs | 10 + RobotNet.MapShares/Enums/EditorState.cs | 16 + RobotNet.MapShares/Enums/TrajectoryDegree.cs | 8 + RobotNet.MapShares/Enums/ZoneType.cs | 34 + RobotNet.MapShares/JsonOptionExtends.cs | 18 + RobotNet.MapShares/MapEditorHelper.cs | 259 ++++ RobotNet.MapShares/MapManagerExtensions.cs | 43 + .../Models/ElementExpressionModel.cs | 8 + RobotNet.MapShares/Models/LoggerModel.cs | 40 + .../Models/MapEditorBackupModel.cs | 75 + .../Property/ElementProperty.cs | 42 + RobotNet.MapShares/RobotNet.MapShares.csproj | 14 + .../OpenIddictClientProviderOptions.cs | 12 + .../OpenIddictResourceOptions.cs | 7 + .../RobotNet.OpenIddictClient.csproj | 9 + .../Controllers/OpenACSSettingsController.cs | 103 ++ .../Controllers/RobotManagerController.cs | 179 +++ .../RobotManagerLoggerController.cs | 87 ++ .../Controllers/RobotModelsController.cs | 258 ++++ .../Controllers/RobotsController.cs | 246 +++ .../TrafficACSRequestController.cs | 25 + .../Controllers/TrafficManagerController.cs | 52 + .../20250509040621_AddRobotDb.Designer.cs | 131 ++ .../Migrations/20250509040621_AddRobotDb.cs | 72 + .../20250509071716_fixRobotDb.Designer.cs | 131 ++ .../Migrations/20250509071716_fixRobotDb.cs | 60 + .../RobotEditorDbContextModelSnapshot.cs | 128 ++ RobotNet.RobotManager/Data/Robot.cs | 40 + .../Data/RobotEditorDbContext.cs | 21 + .../Data/RobotManagerDbExtensions.cs | 17 + RobotNet.RobotManager/Data/RobotModel.cs | 51 + RobotNet.RobotManager/Dockerfile | 73 + .../HubClients/MapHubClient.cs | 41 + .../HubClients/OpenIddictHubClient.cs | 23 + RobotNet.RobotManager/Hubs/RobotHub.cs | 178 +++ RobotNet.RobotManager/Hubs/RobotManagerHub.cs | 244 +++ RobotNet.RobotManager/Hubs/TrafficHub.cs | 70 + RobotNet.RobotManager/Program.cs | 159 ++ .../Properties/launchSettings.json | 15 + .../RobotNet.RobotManager.csproj | 35 + .../RobotNet.RobotManager.http | 6 + .../Services/IRobotController.cs | 36 + RobotNet.RobotManager/Services/IRobotOrder.cs | 15 + .../Services/JsonOptionExtends.cs | 18 + .../Services/LoggerController.cs | 108 ++ RobotNet.RobotManager/Services/MapManager.cs | 267 ++++ .../Services/MapManagerAccessTokenHandler.cs | 33 + RobotNet.RobotManager/Services/MqttBroker.cs | 119 ++ .../Services/OpenACS/ACSHeader.cs | 16 + .../Services/OpenACS/ACSStatusResponse.cs | 24 + .../Services/OpenACS/AGVLocation.cs | 23 + .../Services/OpenACS/AGVState.cs | 15 + .../Services/OpenACS/OpenACSException.cs | 8 + .../Services/OpenACS/OpenACSManager.cs | 102 ++ .../Services/OpenACS/OpenACSPublisher.cs | 171 +++ .../Services/OpenACS/RobotPublishStatus.cs | 15 + .../OpenACS/RobotPublishStatusBody.cs | 71 + .../Services/OpenACS/TrafficACS.cs | 108 ++ .../Services/OpenACS/TrafficACSRequest.cs | 42 + .../Services/OpenACS/TrafficACSResponse.cs | 62 + RobotNet.RobotManager/Services/PathPlanner.cs | 351 +++++ .../Services/Planner/AStar/AStarNode.cs | 28 + .../Planner/AStar/AStarPathPlanner.cs | 393 +++++ .../Differential/DifferentialPathPlanner.cs | 266 ++++ .../Planner/Fokrlift/ForkliftPathPlanner.cs | 596 ++++++++ .../Services/Planner/Fokrlift/TStructure.cs | 135 ++ .../ForkliftV2/ForkLiftPathPlannerV2.cs | 310 ++++ .../Planner/ForkliftV2/SSEAStarNode.cs | 31 + .../Planner/ForkliftV2/SSEAStarPathPlanner.cs | 621 ++++++++ .../Services/Planner/IPathPlanner.cs | 20 + .../Services/Planner/IPathPlannerManager.cs | 8 + .../Planner/OmniDrive/OmniDrivePathPlanner.cs | 265 ++++ .../Services/Planner/PathPlannerManager.cs | 19 + .../Services/Planner/PathPlanningOptions.cs | 8 + .../Services/Planner/PriorityQueue.cs | 23 + .../Services/Planner/Space/KDTree.cs | 61 + .../Services/Planner/Space/MapCompute.cs | 86 ++ .../Services/Planner/Space/RTree.cs | 206 +++ .../Services/Robot/RobotController.cs | 750 +++++++++ .../Services/Robot/RobotOrder.cs | 593 +++++++ .../Services/RobotEditorStorageRepository.cs | 129 ++ .../Services/RobotManager.cs | 562 +++++++ .../Services/RobotPublisher.cs | 105 ++ .../Simulation/Algorithm/FuzzyLogic.cs | 252 +++ .../Simulation/Algorithm/MathExtension.cs | 8 + .../Services/Simulation/Algorithm/PID.cs | 42 + .../Simulation/Algorithm/PurePursuit.cs | 159 ++ .../DifferentialNavigationService.cs | 169 ++ .../Simulation/ForkliftNavigationSevice.cs | 140 ++ .../GridDifferentialNavigationService.cs | 172 +++ .../Services/Simulation/INavigationService.cs | 18 + .../Simulation/Models/NavigationAction.cs | 9 + .../Simulation/Models/NavigationNode.cs | 40 + .../Simulation/Models/NavigationStateType.cs | 12 + .../Simulation/Models/RobotSimulationModel.cs | 16 + .../Services/Simulation/NavigationManager.cs | 15 + .../Services/Simulation/NavigationService.cs | 655 ++++++++ .../Services/Simulation/RobotSimulation.cs | 340 +++++ .../Services/Simulation/VelocityController.cs | 75 + .../Simulation/VisualizationService.cs | 54 + .../Services/Traffic/Agent.cs | 269 ++++ .../Services/Traffic/AgentModel.cs | 13 + .../Services/Traffic/TrafficAlarm.cs | 5 + .../Services/Traffic/TrafficConflict.cs | 15 + .../Services/Traffic/TrafficEdgeDto.cs | 20 + .../Services/Traffic/TrafficGiveway.cs | 13 + .../Services/Traffic/TrafficManager.cs | 857 +++++++++++ .../Services/Traffic/TrafficMap.cs | 26 + .../Services/Traffic/TrafficMath.cs | 158 ++ .../Services/Traffic/TrafficPublisher.cs | 84 + .../Services/Traffic/TrafficSolution.cs | 14 + RobotNet.RobotManager/Services/WatchTimer.cs | 76 + .../Services/WatchTimerAsync.cs | 76 + RobotNet.RobotManager/appsettings.json | 78 + RobotNet.RobotManager/nlog.config | 34 + RobotNet.RobotShares/Dtos/RobotActionDto.cs | 13 + RobotNet.RobotShares/Dtos/RobotDto.cs | 37 + .../Dtos/RobotInfomationDto.cs | 44 + RobotNet.RobotShares/Dtos/RobotModelDto.cs | 37 + .../Dtos/RobotOnlineStateDto.cs | 11 + RobotNet.RobotShares/Dtos/RobotOrderDto.cs | 11 + .../Dtos/RobotVDA5050StateDto.cs | 18 + RobotNet.RobotShares/Dtos/TrafficAgentDto.cs | 17 + .../Dtos/TrafficLockedShapeDto.cs | 22 + RobotNet.RobotShares/Dtos/TrafficMapDto.cs | 13 + RobotNet.RobotShares/Dtos/TrafficNodeDto.cs | 27 + .../Enums/MonitorToolbarButtonType.cs | 9 + .../Enums/MonitorToolbarCheckedType.cs | 11 + RobotNet.RobotShares/Enums/NavigationType.cs | 9 + .../Enums/RefreshPathState.cs | 9 + RobotNet.RobotShares/Enums/RobotDirection.cs | 9 + .../Enums/TrafficConflictState.cs | 49 + .../Enums/TrafficSolutionState.cs | 12 + .../Models/RobotInstantActionModel.cs | 7 + .../Models/RobotMoveStraightModel.cs | 8 + .../Models/RobotMoveToNodeModel.cs | 11 + .../Models/RobotRotateModel.cs | 11 + .../Models/RobotSearchExpressionModel.cs | 8 + .../Models/RobotStateModel.cs | 25 + .../Models/UpdateAgentLockerModel.cs | 10 + .../OpenACS/OpenACSPublishSettingDto.cs | 19 + .../OpenACS/OpenACSSettingsDto.cs | 7 + .../OpenACS/OpenACSTrafficSettingDto.cs | 17 + .../OpenACS/RobotACSLockedDto.cs | 7 + .../OpenACS/TrafficACSRequestModel.cs | 8 + .../OpenACS/TrafficRequestType.cs | 20 + .../RobotNet.RobotShares.csproj | 13 + .../VDA5050/Connection/ConnectionMsg.cs | 28 + .../VDA5050/Factsheet/ActionParameters.cs | 25 + .../VDA5050/Factsheet/AgvActions.cs | 23 + .../VDA5050/Factsheet/AgvGeometry.cs | 12 + .../VDA5050/Factsheet/BoundingBoxReference.cs | 15 + .../VDA5050/Factsheet/Envelopes2d.cs | 21 + .../VDA5050/Factsheet/Envelopes3d.cs | 16 + .../VDA5050/Factsheet/FactSheetMsg.cs | 30 + .../VDA5050/Factsheet/LoadDimensions.cs | 13 + .../VDA5050/Factsheet/LoadSets.cs | 26 + .../VDA5050/Factsheet/LoadSpecification.cs | 9 + .../Factsheet/LocalizationParameter.cs | 6 + .../VDA5050/Factsheet/MaxArrayLens.cs | 21 + .../VDA5050/Factsheet/MaxStringLens.cs | 12 + .../VDA5050/Factsheet/OptionalParameters.cs | 19 + .../VDA5050/Factsheet/PhysicalParameters.cs | 22 + .../VDA5050/Factsheet/ProtocolFeatures.cs | 13 + .../VDA5050/Factsheet/ProtocolLimits.cs | 14 + .../VDA5050/Factsheet/Timing.cs | 13 + .../VDA5050/Factsheet/TypeSpecification.cs | 50 + .../VDA5050/Factsheet/WheelDefinitions.cs | 39 + .../VDA5050/FactsheetExtend/Battery.cs | 9 + .../FactsheetExtend/BatteryThreshold.cs | 11 + .../VDA5050/FactsheetExtend/CameraSafety.cs | 31 + .../VDA5050/FactsheetExtend/ChargerParam.cs | 9 + .../FactsheetExtend/FactsheetExtendMsg.cs | 19 + .../VDA5050/FactsheetExtend/ForkSafety.cs | 9 + .../VDA5050/FactsheetExtend/Initpose.cs | 9 + .../VDA5050/FactsheetExtend/LineSegment.cs | 10 + .../VDA5050/FactsheetExtend/Localization.cs | 14 + .../VDA5050/FactsheetExtend/Motor.cs | 9 + .../VDA5050/FactsheetExtend/Navigation.cs | 11 + .../VDA5050/FactsheetExtend/PPA.cs | 7 + .../VDA5050/FactsheetExtend/PTA.cs | 15 + .../VDA5050/FactsheetExtend/RobotParam.cs | 12 + .../VDA5050/FactsheetExtend/Rotate.cs | 9 + .../VDA5050/FactsheetExtend/Safety.cs | 8 + .../VDA5050/FactsheetExtend/ServerParam.cs | 14 + .../VDA5050/FactsheetExtend/VlMarker.cs | 21 + .../VDA5050/FactsheetExtend/Xloc.cs | 16 + .../VDA5050/InstantAction/ActionParameter.cs | 12 + .../VDA5050/InstantAction/Actions.cs | 23 + .../InstantAction/InstantActionsMsg.cs | 13 + .../VDA5050/Order/Corridor.cs | 18 + RobotNet.RobotShares/VDA5050/Order/Edge.cs | 33 + RobotNet.RobotShares/VDA5050/Order/EdgeLog.cs | 18 + RobotNet.RobotShares/VDA5050/Order/Node.cs | 19 + RobotNet.RobotShares/VDA5050/Order/NodeLog.cs | 19 + .../VDA5050/Order/NodePosition.cs | 19 + .../VDA5050/Order/OrderLog.cs | 21 + .../VDA5050/Order/OrderMsg.cs | 29 + .../VDA5050/Order/Trajectory.cs | 23 + .../VDA5050/State/ActionState.cs | 25 + .../VDA5050/State/BatteryState.cs | 15 + .../VDA5050/State/EdgeState.cs | 19 + RobotNet.RobotShares/VDA5050/State/Error.cs | 29 + .../VDA5050/State/Information.cs | 27 + RobotNet.RobotShares/VDA5050/State/Load.cs | 16 + RobotNet.RobotShares/VDA5050/State/Map.cs | 20 + .../VDA5050/State/NodeState.cs | 30 + .../VDA5050/State/SafetyState.cs | 20 + .../VDA5050/State/StateMsg.cs | 61 + .../VDA5050/Type/ActionType.cs | 35 + .../VDA5050/Type/InformationType.cs | 23 + RobotNet.RobotShares/VDA5050/Type/LoadType.cs | 7 + .../VDA5050/Type/ManualActionType.cs | 17 + RobotNet.RobotShares/VDA5050/VDA5050Helper.cs | 29 + .../VDA5050/VDA5050Setting.cs | 19 + RobotNet.RobotShares/VDA5050/VDA5050Topic.cs | 12 + .../VDA5050/Visualization/AgvPosition.cs | 21 + .../VDA5050/Visualization/Velocity.cs | 8 + .../VDA5050/Visualization/Visualizationmsg.cs | 16 + .../ElementProperties.cs | 49 + .../RobotNet.Script.Expressions.csproj | 11 + RobotNet.Script.Expressions/RobotState.cs | 14 + RobotNet.Script.Shares/CreateModel.cs | 3 + .../Dashboard/DailyMissionDto.cs | 23 + .../Dashboard/DailyPerformanceDto.cs | 14 + .../Dashboard/DashboardDto.cs | 11 + .../Dashboard/TaktTimeMissionDto.cs | 14 + RobotNet.Script.Shares/IRobotNetGlobals.cs | 16 + .../InstanceMissionCreateModel.cs | 3 + RobotNet.Script.Shares/InstanceMissionDto.cs | 11 + RobotNet.Script.Shares/MissionStatus.cs | 14 + RobotNet.Script.Shares/ProcessorRequest.cs | 29 + RobotNet.Script.Shares/ProcessorState.cs | 44 + RobotNet.Script.Shares/RenameModel.cs | 3 + .../RobotNet.Script.Shares.csproj | 14 + RobotNet.Script.Shares/ScriptExtensions.cs | 93 ++ RobotNet.Script.Shares/ScriptFile.cs | 3 + RobotNet.Script.Shares/ScriptFolder.cs | 3 + RobotNet.Script.Shares/ScriptMissionDto.cs | 4 + RobotNet.Script.Shares/ScriptTaskDto.cs | 3 + RobotNet.Script.Shares/ScriptVariableDto.cs | 3 + RobotNet.Script.Shares/UpdateModel.cs | 3 + RobotNet.Script.Shares/UpdateVariableModel.cs | 3 + RobotNet.Script/ICcLinkIeBasicClient.cs | 415 +++++ RobotNet.Script/IConnectionManager.cs | 23 + RobotNet.Script/ILogger.cs | 25 + RobotNet.Script/IMapManager.cs | 154 ++ RobotNet.Script/IModbusTcpClient.cs | 92 ++ RobotNet.Script/IRobot.cs | 232 +++ RobotNet.Script/IUnixDevice.cs | 22 + RobotNet.Script/MissionAttribute.cs | 21 + RobotNet.Script/RobotNet.Script.csproj | 15 + RobotNet.Script/TaskAttribute.cs | 20 + RobotNet.Script/VariableAttribute.cs | 14 + .../Clients/RobotManagerHubClient.cs | 330 ++++ .../Connections/CcLinkIeBasicClient.cs | 679 +++++++++ .../Connections/ModbusTcpClient.cs | 488 ++++++ .../Connections/UnixDevice.cs | 64 + .../Controllers/DashboardConfigController.cs | 39 + .../Controllers/ScriptController.cs | 197 +++ .../ScriptManagerLoggerController.cs | 47 + .../Controllers/ScriptMissionsController.cs | 154 ++ .../Controllers/ScriptTasksController.cs | 18 + .../Controllers/ScriptVariablesController.cs | 72 + .../Data/InstanceMission.cs | 39 + ...458_InitScriptManagerDbContext.Designer.cs | 61 + ...250630100458_InitScriptManagerDbContext.cs | 42 + ...AddParametersToInstanceMission.Designer.cs | 65 + ...01130724_AddParametersToInstanceMission.cs | 28 + ...9_AddStopedAtToInstanceMission.Designer.cs | 69 + ...0814072909_AddStopedAtToInstanceMission.cs | 30 + .../ScriptManagerDbContextModelSnapshot.cs | 66 + .../Data/ScriptManagerDbContext.cs | 16 + .../Data/ScriptManagerDbExtensions.cs | 17 + RobotNet.ScriptManager/Dockerfile | 105 ++ .../Helpers/CSharpSyntaxHelper.cs | 707 +++++++++ .../Helpers/ScriptConfiguration.cs | 226 +++ .../Helpers/VDA5050Helper.cs | 74 + RobotNet.ScriptManager/Hubs/ConsoleHub.cs | 20 + RobotNet.ScriptManager/Hubs/DashboardHub.cs | 24 + RobotNet.ScriptManager/Hubs/HMIHub.cs | 45 + RobotNet.ScriptManager/Hubs/ProcessorHub.cs | 47 + RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs | 68 + RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs | 33 + RobotNet.ScriptManager/Hubs/VariablesHub.cs | 78 + RobotNet.ScriptManager/Models/ConsoleLog.cs | 25 + .../Models/ScriptGlobalsRobotNet.cs | 63 + .../Models/ScriptMapElement.cs | 109 ++ .../Models/ScriptMapManager.cs | 319 ++++ .../Models/ScriptMapNode.cs | 3 + .../Models/ScriptMission.cs | 298 ++++ .../Models/ScriptMissionData.cs | 4 + .../Models/ScriptMissionLogger.cs | 47 + .../Models/ScriptRobotManager.cs | 185 +++ RobotNet.ScriptManager/Models/ScriptTask.cs | 155 ++ .../Models/ScriptTaskData.cs | 3 + .../Models/ScriptTaskLogger.cs | 22 + .../Models/ScriptVariable.cs | 23 + RobotNet.ScriptManager/Program.cs | 139 ++ .../Properties/launchSettings.json | 15 + .../RobotNet.ScriptManager.csproj | 29 + .../Services/DashboardConfig.cs | 36 + .../Services/DashboardPublisher.cs | 115 ++ .../Services/LoopService.cs | 45 + .../Services/ScriptConnectionManager.cs | 80 + .../Services/ScriptGlobalsManager.cs | 231 +++ .../Services/ScriptMissionCreator.cs | 63 + .../Services/ScriptMissionManager.cs | 410 +++++ .../Services/ScriptStateManager.cs | 298 ++++ .../Services/ScriptTaskManager.cs | 91 ++ RobotNet.ScriptManager/appsettings.json | 44 + RobotNet.ScriptManager/nlog.config | 24 + RobotNet.ScriptManager/readme | 5 + .../dlls/RobotNet.Script.Expressions.dll | Bin 0 -> 10240 bytes .../dlls/RobotNet.Script.Expressions.xml | 93 ++ .../wwwroot/dlls/RobotNet.Script.dll | Bin 0 -> 35840 bytes .../wwwroot/dlls/RobotNet.Script.xml | 1075 +++++++++++++ .../wwwroot/dlls/System.Collections.dll | Bin 0 -> 56592 bytes .../wwwroot/dlls/System.Linq.Expressions.dll | Bin 0 -> 63792 bytes .../wwwroot/dlls/System.Runtime.dll | Bin 0 -> 847648 bytes RobotNet.ServiceDefaults/Dockerfile | 0 RobotNet.ServiceDefaults/Extensions.cs | 119 ++ .../RobotNet.ServiceDefaults.csproj | 22 + RobotNet.Shares/MessageResult.cs | 8 + RobotNet.Shares/RobotNet.Shares.csproj | 9 + RobotNet.Shares/SearchResult.cs | 9 + .../AppJsonSerializerContext.cs | 12 + RobotNet.SystemUpgrade/Program.cs | 105 ++ .../Properties/launchSettings.json | 16 + .../RobotNet.SystemUpgrade.csproj | 11 + .../RobotNet.SystemUpgrade.sln | 25 + .../appsettings.Development.json | 8 + RobotNet.SystemUpgrade/appsettings.json | 9 + RobotNet.SystemUpgrade/libman.json | 15 + RobotNet.SystemUpgrade/readme.md | 1 + RobotNet.SystemUpgrade/wwwroot/favicon.svg | 5 + RobotNet.SystemUpgrade/wwwroot/index.html | 553 +++++++ RobotNet.WebApp/App.razor | 25 + .../Charts/Components/BarChart.razor | 7 + .../Charts/Components/BarChart.razor.cs | 115 ++ .../Charts/Components/BubbleChart.razor | 7 + .../Charts/Components/BubbleChart.razor.cs | 115 ++ .../Charts/Components/ComboBarLineChart.razor | 7 + .../Components/ComboBarLineChart.razor.cs | 115 ++ .../Charts/Components/LineChart.razor | 7 + .../Charts/Components/LineChart.razor.cs | 115 ++ .../Charts/Components/PieChart.razor | 7 + .../Charts/Components/PieChart.razor.cs | 118 ++ .../Charts/Components/PolarAreaChart.razor | 7 + .../Charts/Components/PolarAreaChart.razor.cs | 115 ++ .../Charts/Components/RadarChart.razor | 7 + .../Charts/Components/RadarChart.razor.cs | 115 ++ .../Charts/Components/ScatterChart.razor | 7 + .../Charts/Components/ScatterChart.razor.cs | 115 ++ .../Charts/Core/BlazorComponentBase.cs | 149 ++ RobotNet.WebApp/Charts/Core/ChartColors.cs | 22 + RobotNet.WebApp/Charts/Core/RobotNetChart.cs | 80 + RobotNet.WebApp/Charts/Enums/AxisDirection.cs | 11 + RobotNet.WebApp/Charts/Enums/BorderAlign.cs | 7 + .../Charts/Enums/BorderJoinStyle.cs | 12 + RobotNet.WebApp/Charts/Enums/BorderSkipped.cs | 17 + RobotNet.WebApp/Charts/Enums/ChartAxesType.cs | 13 + RobotNet.WebApp/Charts/Enums/ChartType.cs | 17 + .../Charts/Enums/DataLabelsAlignment.cs | 16 + .../Charts/Enums/DataLabelsAnchoring.cs | 12 + RobotNet.WebApp/Charts/Enums/DatasetType.cs | 10 + RobotNet.WebApp/Charts/Enums/Easing.cs | 40 + .../Charts/Enums/EnumExtensions.cs | 38 + RobotNet.WebApp/Charts/Enums/IndexAxis.cs | 10 + .../Charts/Enums/InteractionMode.cs | 37 + .../Charts/Enums/LegendAlignment.cs | 11 + .../Charts/Enums/LegendPosition.cs | 16 + .../Charts/Enums/LowerCaseEnumConverter.cs | 52 + RobotNet.WebApp/Charts/Enums/PointStyle.cs | 18 + .../Charts/Enums/TicksAlignment.cs | 11 + .../Charts/Enums/TitleAlignment.cs | 11 + RobotNet.WebApp/Charts/Enums/TitlePosition.cs | 12 + .../Charts/Enums/TooltipAlignment.cs | 19 + .../Charts/Enums/TooltipPosition.cs | 11 + RobotNet.WebApp/Charts/Enums/Unit.cs | 57 + .../Charts/Models/BarChart/Axes/ChartAxes.cs | 86 ++ .../Models/BarChart/Axes/ChartAxesBorder.cs | 41 + .../Models/BarChart/Axes/ChartAxesGrid.cs | 82 + .../Models/BarChart/Axes/ChartAxesTicks.cs | 64 + .../BarChart/Axes/ChartAxesTicksMajor.cs | 10 + .../Models/BarChart/Axes/ChartAxesTitle.cs | 44 + .../Charts/Models/BarChart/BarChartDataset.cs | 118 ++ .../Models/BarChart/BarChartDatasetData.cs | 7 + .../Charts/Models/BarChart/BarChartOptions.cs | 21 + .../Charts/Models/BarChart/BarChartPlugins.cs | 7 + .../Charts/Models/BarChart/BarScales.cs | 13 + .../Models/BubbleChart/BubbleChartDataset.cs | 18 + .../BubbleChart/BubbleChartDatasetData.cs | 7 + .../Models/BubbleChart/BubbleChartOptions.cs | 21 + .../Models/BubbleChart/BubbleChartPlugins.cs | 7 + .../Charts/Models/BubbleChart/BubblePoint.cs | 26 + .../ComboBarLineChart/ComboBarLineDataset.cs | 116 ++ .../ComboBarLineDatasetData.cs | 7 + .../ComboBarLineChart/ComboBarLineOptions.cs | 21 + .../ComboBarLineChart/ComboBarLinePlugins.cs | 7 + .../ComboBarLineChart/ComboBarLineScales.cs | 16 + .../Charts/Models/Common/ArcAnimation.cs | 19 + .../Charts/Models/Common/ChartAnimation.cs | 28 + .../Charts/Models/Common/ChartFont.cs | 36 + .../Charts/Models/Common/ChartLayout.cs | 17 + .../Charts/Models/Common/ChartOptions.cs | 67 + .../Charts/Models/Common/ChartPadding.cs | 24 + .../Charts/Models/Common/Dataset/ChartData.cs | 12 + .../Models/Common/Dataset/ChartDataset.cs | 256 ++++ .../Models/Common/Dataset/ChartDatasetData.cs | 10 + .../Common/Dataset/ChartDatasetDataLabels.cs | 24 + .../Charts/Models/Common/Elements/Arc.cs | 55 + .../Charts/Models/Common/Elements/Bar.cs | 46 + .../Models/Common/Elements/ChartElements.cs | 30 + .../Charts/Models/Common/Elements/Line.cs | 71 + .../Charts/Models/Common/Elements/Point.cs | 54 + .../Charts/Models/Common/IChartDataset.cs | 5 + .../Charts/Models/Common/IChartDatasetData.cs | 5 + .../Charts/Models/Common/IChartOptions.cs | 5 + .../Charts/Models/Common/Interaction.cs | 67 + .../Models/Common/Plugins/ChartPlugins.cs | 35 + .../Common/Plugins/ChartPluginsDataLabels.cs | 37 + .../Common/Plugins/ChartPluginsLegend.cs | 67 + .../Plugins/ChartPluginsLegendLabels.cs | 67 + .../Common/Plugins/ChartPluginsLegendTitle.cs | 35 + .../Common/Plugins/ChartPluginsSubtitle.cs | 42 + .../Common/Plugins/ChartPluginsTitle.cs | 42 + .../Common/Plugins/ChartPluginsTooltip.cs | 176 +++ .../Models/LineChart/LineChartDataset.cs | 216 +++ .../Models/LineChart/LineChartDatasetData.cs | 7 + .../Models/LineChart/LineChartOptions.cs | 21 + .../Models/LineChart/LineChartPlugins.cs | 7 + .../Charts/Models/PieChart/PieChartDataset.cs | 7 + .../Models/PieChart/PieChartDatasetData.cs | 7 + .../Charts/Models/PieChart/PieChartOptions.cs | 40 + .../Charts/Models/PieChart/PieChartPlugins.cs | 7 + .../PolarAreaChart/PolarAreaChartDataset.cs | 8 + .../PolarAreaChartDatasetData.cs | 7 + .../PolarAreaChart/PolarAreaChartOptions.cs | 12 + .../PolarAreaChart/PolarAreaChartPlugins.cs | 7 + .../Models/RadarChart/Axis/AngleLines.cs | 36 + .../Models/RadarChart/Axis/GridLines.cs | 90 ++ .../Models/RadarChart/Axis/PointLabels.cs | 47 + .../Models/RadarChart/Axis/RadarAxis.cs | 41 + .../Models/RadarChart/Axis/RadarScale.cs | 6 + .../Models/RadarChart/Axis/RadialTicks.cs | 67 + .../Models/RadarChart/RadarChartDataset.cs | 156 ++ .../RadarChart/RadarChartDatasetData.cs | 7 + .../Models/RadarChart/RadarChartOptions.cs | 20 + .../Models/RadarChart/RadarChartPlugins.cs | 7 + .../ScatterChart/ScatterChartDataPoint.cs | 4 + .../ScatterChart/ScatterChartDataset.cs | 222 +++ .../ScatterChart/ScatterChartDatasetData.cs | 7 + .../ScatterChart/ScatterChartOptions.cs | 22 + .../ScatterChart/ScatterChartPlugins.cs | 7 + RobotNet.WebApp/Clients/ConsoleHubClient.cs | 46 + RobotNet.WebApp/Clients/DashboardHubClient.cs | 30 + RobotNet.WebApp/Clients/HMIHubClient.cs | 28 + RobotNet.WebApp/Clients/ProcessorHubClient.cs | 29 + RobotNet.WebApp/Clients/RobotHubClient.cs | 102 ++ .../Clients/ScriptTaskHubClient.cs | 25 + RobotNet.WebApp/Clients/TrafficHubClient.cs | 45 + .../Clients/WebAssemblyHubClient.cs | 12 + .../Components/ConfirmDialog.razor | 33 + .../Components/DailyComponentData.razor | 29 + .../Components/DailyComponentData.razor.css | 47 + .../Dashboard/Components/DailyData.razor | 29 + .../MissionsPerformanceBarChart.razor | 134 ++ .../MissionsPerformanceBarChart.razor.css | 30 + .../Components/PerformancePieChart.razor | 90 ++ .../Components/PerformancePieChart.razor.css | 44 + .../Components/TaktTimeLineChart.razor | 122 ++ .../Components/TaktTimeLineChart.razor.css | 30 + RobotNet.WebApp/Dockerfile | 61 + ...ersistentStorageConfigurationExtensions.cs | 23 + .../Helpers/ServiceCollectionExtensions.cs | 27 + RobotNet.WebApp/Layout/HMILayout.razor | 12 + RobotNet.WebApp/Layout/MainLayout.razor | 13 + RobotNet.WebApp/Layout/NavMenu.razor | 67 + RobotNet.WebApp/Layout/NavMenu.razor.css | 117 ++ RobotNet.WebApp/Layout/RedirectToLogin.razor | 9 + .../Maps/Components/Editor/Edge/Edge.razor | 121 ++ .../Components/Editor/Edge/Edge.razor.css | 24 + .../Editor/Edge/EdgeControlPoint.razor | 128 ++ .../Editor/Edge/EdgeControlPoint.razor.css | 23 + .../Editor/Edge/EdgeCurveCreating.razor | 163 ++ .../Editor/Edge/EdgeCurveCreating.razor.css | 12 + .../Editor/Edge/EdgeDirection.razor | 113 ++ .../Editor/Edge/EdgeDirection.razor.css | 5 + .../Editor/Edge/EdgeStraightCreating.razor | 46 + .../Edge/EdgeStraightCreating.razor.css | 12 + .../Maps/Components/Editor/Edge/MapEdge.razor | 255 ++++ .../Components/Editor/Edge/MapEdge.razor.css | 13 + .../Components/Editor/Element/Element.razor | 113 ++ .../Editor/Element/Element.razor.css | 21 + .../Maps/Components/Editor/MapContainer.razor | 558 +++++++ .../Components/Editor/MapContainer.razor.cs | 1092 +++++++++++++ .../Components/Editor/MapContainer.razor.css | 28 + .../Maps/Components/Editor/MapCopy.razor | 121 ++ .../Maps/Components/Editor/MapGrid.razor | 37 + .../Maps/Components/Editor/MapGrid.razor.css | 6 + .../Components/Editor/MapMousePosition.razor | 20 + .../Editor/MapMousePosition.razor.css | 14 + .../Maps/Components/Editor/MapScaner.razor | 40 + .../Components/Editor/MapScaner.razor.css | 5 + .../Maps/Components/Editor/MapSvgDefs.razor | 32 + .../Components/Editor/MapSvgDefs.razor.css | 5 + .../Maps/Components/Editor/Node/MapNode.razor | 489 ++++++ .../Components/Editor/Node/MapNode.razor.css | 44 + .../Maps/Components/Editor/Node/Node.razor | 102 ++ .../Components/Editor/Node/Node.razor.css | 41 + .../Maps/Components/Editor/OriginVector.razor | 10 + .../Components/Editor/OriginVector.razor.css | 4 + .../Maps/Components/Editor/Zone/MapZone.razor | 156 ++ .../Maps/Components/Editor/Zone/Zone.razor | 95 ++ .../Components/Editor/Zone/Zone.razor.css | 29 + .../Editor/Zone/ZoneControlPoint.razor | 137 ++ .../Editor/Zone/ZoneControlPoint.razor.css | 31 + .../Components/Editor/Zone/ZoneCreating.razor | 92 ++ .../Editor/Zone/ZoneCreating.razor.css | 12 + .../Element/ElementDefaultProperty.razor | 238 +++ .../Components/Element/ElementImage.razor | 187 +++ .../Components/Element/ElementImage.razor.css | 31 + .../Element/ElementModelTable.razor | 341 +++++ .../Element/ElementModelTable.razor.css | 16 + .../Element/ElementPropertyTable.razor | 193 +++ .../Maps/Components/Element/MapElement.razor | 288 ++++ .../Components/NavigationMapPreview.razor | 207 +++ .../Components/NavigationMapPreview.razor.css | 42 + .../Components/Setting/MapSettingAction.razor | 390 +++++ .../Setting/MapSettingAction.razor.css | 24 + .../Setting/MapSettingDefault.razor | 277 ++++ .../Setting/MapSettingDefault.razor.css | 6 + .../Components/Toolbar/AlignmentToolbar.razor | 81 + .../Toolbar/AlignmentToolbar.razor.css | 19 + .../Components/Toolbar/ControlToolbar.razor | 209 +++ .../Toolbar/ControlToolbar.razor.css | 63 + .../Toolbar/EditorFunctionToolbar.razor | 106 ++ .../Toolbar/EditorFunctionToolbar.razor.css | 16 + .../Components/Toolbar/EditorToolbar.razor | 62 + .../Maps/Components/_Imports.razor | 8 + RobotNet.WebApp/Maps/Models/EdgeModel.cs | 131 ++ RobotNet.WebApp/Maps/Models/ElementModel.cs | 60 + RobotNet.WebApp/Maps/Models/MapEdgeModel.cs | 123 ++ .../Maps/Models/MapElementModel.cs | 45 + RobotNet.WebApp/Maps/Models/MapNodeModel.cs | 86 ++ RobotNet.WebApp/Maps/Models/MapZoneModel.cs | 73 + RobotNet.WebApp/Maps/Models/NodeModel.cs | 59 + RobotNet.WebApp/Maps/Models/ZoneModel.cs | 121 ++ RobotNet.WebApp/Pages/Authentication.razor | 7 + RobotNet.WebApp/Pages/Dashboard.razor | 105 ++ RobotNet.WebApp/Pages/Dashboard.razor.css | 115 ++ RobotNet.WebApp/Pages/Logs.razor | 192 +++ RobotNet.WebApp/Pages/Logs.razor.css | 38 + RobotNet.WebApp/Pages/Missions.razor | 303 ++++ .../Pages/NavigationMapEditor.razor | 196 +++ .../Pages/NavigationMapElement.razor | 103 ++ .../Pages/NavigationMapSetting.razor | 116 ++ .../Pages/NavigationMapsManager.razor | 585 +++++++ .../Pages/NavigationMapsManager.razor.css | 21 + RobotNet.WebApp/Pages/OpenACSSettings.razor | 71 + RobotNet.WebApp/Pages/RobotDetail.razor | 108 ++ RobotNet.WebApp/Pages/RobotManager.razor | 315 ++++ RobotNet.WebApp/Pages/RobotModelManager.razor | 464 ++++++ .../Pages/RobotModelManager.razor.css | 21 + RobotNet.WebApp/Pages/RobotMonitoring.razor | 251 +++ .../Pages/RobotTrafficManager.razor | 431 ++++++ .../Pages/RobotTrafficManager.razor.css | 19 + .../Pages/SEHC_DA3_Line1_HMI.razor | 602 ++++++++ .../Pages/SEHC_DA3_Line1_HMI.razor.cs | 37 + .../Pages/SEHC_DA3_Line1_HMI.razor.css | 554 +++++++ RobotNet.WebApp/Pages/ScriptEditor.razor | 121 ++ RobotNet.WebApp/Pages/ScriptEditor.razor.css | 62 + RobotNet.WebApp/Pages/ScriptManager.razor | 38 + RobotNet.WebApp/Pages/User.razor | 25 + RobotNet.WebApp/Program.cs | 48 + .../Properties/launchSettings.json | 16 + RobotNet.WebApp/RobotNet.WebApp.csproj | 37 + .../Components/Monitoring/Element/Edge.razor | 50 + .../Monitoring/Element/Edge.razor.cs | 38 + .../Monitoring/Element/Edge.razor.css | 24 + .../Monitoring/Element/EdgeDirection.razor | 69 + .../Element/EdgeDirection.razor.css | 5 + .../Monitoring/Element/Element.razor | 95 ++ .../Monitoring/Element/Element.razor.cs | 38 + .../Monitoring/Element/Element.razor.css | 21 + .../Components/Monitoring/Element/Node.razor | 47 + .../Monitoring/Element/Node.razor.cs | 21 + .../Monitoring/Element/Node.razor.css | 41 + .../Components/Monitoring/Element/Robot.razor | 111 ++ .../Monitoring/Element/Robot.razor.cs | 43 + .../Monitoring/Element/Robot.razor.css | 13 + .../Components/Monitoring/Element/Zone.razor | 40 + .../Monitoring/Element/Zone.razor.cs | 35 + .../Monitoring/Element/Zone.razor.css | 29 + .../Components/Monitoring/MapGrid.razor | 37 + .../Components/Monitoring/MapGrid.razor.css | 6 + .../Monitoring/MapMousePosition.razor | 20 + .../Monitoring/MapMousePosition.razor.css | 14 + .../Components/Monitoring/MapRobot.razor | 30 + .../Components/Monitoring/MapRobot.razor.cs | 49 + .../Components/Monitoring/MapSvgDefs.razor | 32 + .../Monitoring/MapSvgDefs.razor.css | 5 + .../Monitoring/MonitorInfomation.razor | 125 ++ .../Components/Monitoring/MonitorMap.razor | 501 ++++++ .../Monitoring/MonitorMap.razor.css | 57 + .../Monitoring/MonitorToolbar.razor | 96 ++ .../Monitoring/MonitorToolbar.razor.cs | 11 + .../Components/Monitoring/OriginVector.razor | 10 + .../Monitoring/OriginVector.razor.css | 4 + .../Monitoring/RobotCurrentPath.razor | 35 + .../Monitoring/RobotLaserScaner.razor | 17 + .../Components/Monitoring/RobotPath.razor | 45 + .../Robots/Components/OpenACS/ACSLog.razor | 128 ++ .../Components/OpenACS/ACSLog.razor.css | 57 + .../Components/OpenACS/DashboardConfig.razor | 111 ++ .../Components/OpenACS/PublishSetting.razor | 128 ++ .../OpenACS/TrafficACSLockedView.razor | 59 + .../OpenACS/TrafficACSLockedView.razor.css | 29 + .../Components/OpenACS/TrafficRequest.razor | 104 ++ .../Components/OpenACS/TrafficSetting.razor | 124 ++ .../Robots/Components/Robot/RobotAction.razor | 310 ++++ .../Components/Robot/RobotInfomation.razor | 129 ++ .../Robot/RobotInfomation.razor.css | 19 + .../Components/Robot/RobotModelPreview.razor | 160 ++ .../Robot/RobotModelPreview.razor.css | 43 + .../Components/Robot/RobotTestRandom.razor | 147 ++ .../Robot/RobotTestRandom.razor.css | 13 + .../Traffic/TrafficAgentReview.razor | 123 ++ .../Traffic/TrafficNodePreview.razor | 15 + .../Robots/Components/_Imports.razor | 7 + .../Scripts/Components/ActionButton.razor | 31 + .../Scripts/Components/ActionButton.razor.css | 31 + .../Scripts/Components/ConsoleItem.razor | 34 + .../Scripts/Components/ConsoleItem.razor.css | 21 + .../Dashboards/InstantiateMissionDialog.razor | 216 +++ .../Components/Dashboards/Missions.razor | 259 ++++ .../Dashboards/ProcessController.razor | 127 ++ .../Dashboards/SetVariableValueDialog.razor | 253 +++ .../Scripts/Components/Dashboards/Tasks.razor | 263 ++++ .../Components/Dashboards/Variables.razor | 185 +++ .../Dialogs/CreateNewFileOrFolderDialog.razor | 64 + .../Dialogs/DeleteFileOrFolderDialog.razor | 98 ++ .../Dialogs/RenameFileOrFolderDialog.razor | 107 ++ .../Scripts/Components/Editor.razor | 262 ++++ .../Scripts/Components/EditorHierachy.razor | 113 ++ .../Components/EditorHierachy.razor.css | 20 + .../Scripts/Components/FileItem.razor | 70 + .../Scripts/Components/FileItem.razor.css | 44 + .../Scripts/Components/FolderItem.razor | 96 ++ .../Scripts/Components/FolderItem.razor.css | 47 + .../Components/HierachyItemDiagnostics.razor | 44 + .../HierachyItemDiagnostics.razor.css | 29 + .../Components/HierachyItemModified.razor | 28 + .../Components/HierachyItemModified.razor.css | 14 + .../Scripts/Components/HierachyItemName.razor | 24 + .../Components/HierachyItemName.razor.css | 6 + .../Scripts/Components/Loading.razor | 7 + .../Scripts/Components/ProcessorConsole.razor | 70 + .../Scripts/Components/ProcessorControl.razor | 172 +++ .../Scripts/Components/SidebarActions.razor | 75 + .../Components/SidebarActions.razor.css | 9 + .../Scripts/Components/_Imports.razor | 3 + .../Scripts/Models/ConsoleItemModel.cs | 17 + .../Scripts/Models/HierachyItemModel.cs | 12 + .../Scripts/Models/InstanceMissionModel.cs | 14 + .../Scripts/Models/ScriptFileModel.cs | 80 + .../Scripts/Models/ScriptFolderModel.cs | 101 ++ .../Scripts/Models/ScriptMissionModel.cs | 11 + .../Models/ScriptMissionParameterModel.cs | 247 +++ .../Scripts/Models/ScriptTaskModel.cs | 12 + .../Scripts/Models/ScriptVariableModel.cs | 11 + .../Scripts/Models/ScriptWorkspace.cs | 561 +++++++ .../Documentation/DocumentationComment.cs | 196 +++ .../Documentation/DocumentationConverter.cs | 169 ++ .../Monaco/Documentation/DocumentationItem.cs | 7 + .../Scripts/Monaco/Editor/MarkerData.cs | 16 + .../Monaco/Editor/ModelContentChange.cs | 9 + .../Monaco/Editor/ModelContentChangedEvent.cs | 12 + .../Monaco/Editor/RelatedInformation.cs | 11 + .../Monaco/Editor/SingleEditOperation.cs | 8 + .../Scripts/Monaco/InvocationContext.cs | 34 + .../Scripts/Monaco/Languages/Command.cs | 9 + .../Monaco/Languages/CompletionExtensions.cs | 248 +++ .../Monaco/Languages/CompletionItem.cs | 21 + .../Languages/CompletionItemInsertTextRule.cs | 15 + .../Monaco/Languages/CompletionItemKind.cs | 33 + .../Monaco/Languages/CompletionItemLabel.cs | 8 + .../Monaco/Languages/CompletionItemRanges.cs | 9 + .../Monaco/Languages/CompletionItemTag.cs | 6 + .../Monaco/Languages/CompletionList.cs | 7 + .../Monaco/Languages/ParameterInformation.cs | 7 + .../Scripts/Monaco/Languages/SignatureHelp.cs | 8 + .../Languages/SignatureHelpExtensions.cs | 169 ++ .../Monaco/Languages/SignatureHelpResult.cs | 6 + .../Monaco/Languages/SignatureInformation.cs | 9 + .../Scripts/Monaco/MarkdownHelpers.cs | 260 ++++ .../Scripts/Monaco/MarkdownString.cs | 11 + .../Scripts/Monaco/MarkerSeverity.cs | 9 + RobotNet.WebApp/Scripts/Monaco/MarkerTag.cs | 7 + .../Scripts/Monaco/MonacoExtensions.cs | 150 ++ RobotNet.WebApp/Scripts/Monaco/Range.cs | 9 + .../Scripts/Monaco/StringBuilderExtension.cs | 16 + .../Scripts/Monaco/UriComponents.cs | 10 + RobotNet.WebApp/_Imports.razor | 13 + RobotNet.WebApp/libman.json | 19 + RobotNet.WebApp/nginx.conf | 52 + RobotNet.WebApp/wwwroot/appsettings.json | 48 + RobotNet.WebApp/wwwroot/css/app.css | 147 ++ ...aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 | Bin 0 -> 11840 bytes ...n66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2 | Bin 0 -> 20612 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 | Bin 0 -> 9644 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2 | Bin 0 -> 3676 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2 | Bin 0 -> 16848 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 | Bin 0 -> 13740 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 | Bin 0 -> 7856 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2 | Bin 0 -> 10576 bytes ...aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 | Bin 0 -> 19660 bytes .../wwwroot/css/mud/fonts.googleapis.com.css | 81 + RobotNet.WebApp/wwwroot/favicon.svg | 5 + RobotNet.WebApp/wwwroot/icon-192.png | Bin 0 -> 2626 bytes RobotNet.WebApp/wwwroot/icon-512.png | Bin 0 -> 6311 bytes .../wwwroot/images/Image-not-found.png | Bin 0 -> 809 bytes RobotNet.WebApp/wwwroot/images/logoDark.svg | 10 + RobotNet.WebApp/wwwroot/images/logoLight.svg | 48 + RobotNet.WebApp/wwwroot/index.html | 58 + RobotNet.WebApp/wwwroot/js/app.js | 42 + RobotNet.WebApp/wwwroot/js/chart.umd.js | 14 + RobotNet.WebApp/wwwroot/js/chart.umd.js.map | 1 + .../js/chartjs-plugin-datalabels.esm.js | 1351 ++++++++++++++++ .../wwwroot/js/chartjs-plugin-datalabels.js | 1356 +++++++++++++++++ .../js/chartjs-plugin-datalabels.min.js | 7 + RobotNet.WebApp/wwwroot/js/map.js | 256 ++++ RobotNet.WebApp/wwwroot/js/robonet.chart.js | 176 +++ RobotNet.WebApp/wwwroot/js/robot-design.js | 119 ++ RobotNet.WebApp/wwwroot/js/script.js | 76 + RobotNet.WebApp/wwwroot/manifest.webmanifest | 22 + RobotNet.WebApp/wwwroot/sehc/amr.png | Bin 0 -> 106820 bytes RobotNet.WebApp/wwwroot/sehc/station.png | Bin 0 -> 47514 bytes RobotNet.WebApp/wwwroot/sehc/trolley.png | Bin 0 -> 47708 bytes RobotNet.WebApp/wwwroot/service-worker.js | 4 + .../wwwroot/service-worker.published.js | 55 + RobotNet.sln | 121 ++ appsettings.RobotNet.WebApp.json | 48 + certificate/gencert.cmd | 20 + certificate/gencert.sh | 23 + certificate/san.cnf | 10 + clean.ps1 | 1 + docker-compose.yaml | 153 ++ docker-deploy.yaml | 188 +++ install.md | 85 ++ sehcio/Makefile | 21 + sehcio/sehcio.c | 449 ++++++ 885 files changed, 74595 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 RobotNet.AppHost/Dockerfile create mode 100644 RobotNet.AppHost/Program.cs create mode 100644 RobotNet.AppHost/Properties/launchSettings.json create mode 100644 RobotNet.AppHost/RobotNet.AppHost.csproj create mode 100644 RobotNet.AppHost/appsettings.Development.json create mode 100644 RobotNet.AppHost/appsettings.json create mode 100644 RobotNet.Clients/HttpClientExtensions.cs create mode 100644 RobotNet.Clients/HubClient.cs create mode 100644 RobotNet.Clients/RobotNet.Clients.csproj create mode 100644 RobotNet.IdentityServer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs create mode 100644 RobotNet.IdentityServer/Components/Account/IdentityNoOpEmailSender.cs create mode 100644 RobotNet.IdentityServer/Components/Account/IdentityRedirectManager.cs create mode 100644 RobotNet.IdentityServer/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs create mode 100644 RobotNet.IdentityServer/Components/Account/IdentityUserAccessor.cs create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/AccessLogin.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Infor.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Infor.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Login.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/LogoutConfirm.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictManager.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Password.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Password.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Register.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Role.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/Role.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor.css create mode 100644 RobotNet.IdentityServer/Components/Account/Pages/_Imports.razor create mode 100644 RobotNet.IdentityServer/Components/Account/Shared/RedirectToLogin.razor create mode 100644 RobotNet.IdentityServer/Components/App.razor create mode 100644 RobotNet.IdentityServer/Components/Layout/MainLayout.razor create mode 100644 RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css create mode 100644 RobotNet.IdentityServer/Components/Layout/NavMenu.razor create mode 100644 RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css create mode 100644 RobotNet.IdentityServer/Components/Pages/Error.razor create mode 100644 RobotNet.IdentityServer/Components/Pages/Home.razor create mode 100644 RobotNet.IdentityServer/Components/Routes.razor create mode 100644 RobotNet.IdentityServer/Components/_Imports.razor create mode 100644 RobotNet.IdentityServer/Controllers/AuthorizationController.cs create mode 100644 RobotNet.IdentityServer/Controllers/IdentityServerLoggerController.cs create mode 100644 RobotNet.IdentityServer/Controllers/UserinfoController.cs create mode 100644 RobotNet.IdentityServer/Data/ApplicationDbContext.cs create mode 100644 RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs create mode 100644 RobotNet.IdentityServer/Data/ApplicationRole.cs create mode 100644 RobotNet.IdentityServer/Data/ApplicationUser.cs create mode 100644 RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs create mode 100644 RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.cs create mode 100644 RobotNet.IdentityServer/Data/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 RobotNet.IdentityServer/Dockerfile create mode 100644 RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs create mode 100644 RobotNet.IdentityServer/Helpers/FormValueRequiredAttribute.cs create mode 100644 RobotNet.IdentityServer/Program.cs create mode 100644 RobotNet.IdentityServer/Properties/launchSettings.json create mode 100644 RobotNet.IdentityServer/Properties/serviceDependencies.json create mode 100644 RobotNet.IdentityServer/Properties/serviceDependencies.local.json create mode 100644 RobotNet.IdentityServer/RobotNet.IdentityServer.csproj create mode 100644 RobotNet.IdentityServer/Services/IdentityService.cs create mode 100644 RobotNet.IdentityServer/Services/PasswordStrengthService.cs create mode 100644 RobotNet.IdentityServer/Services/UserImageService.cs create mode 100644 RobotNet.IdentityServer/Services/UserInfoService.cs create mode 100644 RobotNet.IdentityServer/appsettings.json create mode 100644 RobotNet.IdentityServer/libman.json create mode 100644 RobotNet.IdentityServer/nlog.config create mode 100644 RobotNet.IdentityServer/wwwroot/app.css create mode 100644 RobotNet.IdentityServer/wwwroot/favicon.svg create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 create mode 100644 RobotNet.IdentityServer/wwwroot/mud/fonts.googleapis.com.css create mode 100644 RobotNet.IdentityServer/wwwroot/uploads/avatars/anh.jpg create mode 100644 RobotNet.MapManager/Controllers/ActionsController.cs create mode 100644 RobotNet.MapManager/Controllers/EdgesController.cs create mode 100644 RobotNet.MapManager/Controllers/ElementModelsController.cs create mode 100644 RobotNet.MapManager/Controllers/ElementsController.cs create mode 100644 RobotNet.MapManager/Controllers/ImagesController.cs create mode 100644 RobotNet.MapManager/Controllers/MapDesignerLoggerController.cs create mode 100644 RobotNet.MapManager/Controllers/MapExportController.cs create mode 100644 RobotNet.MapManager/Controllers/MapsDataController.cs create mode 100644 RobotNet.MapManager/Controllers/MapsManagerController.cs create mode 100644 RobotNet.MapManager/Controllers/MapsSettingController.cs create mode 100644 RobotNet.MapManager/Controllers/NodesController.cs create mode 100644 RobotNet.MapManager/Controllers/ScriptElementsController.cs create mode 100644 RobotNet.MapManager/Controllers/ZonesController.cs create mode 100644 RobotNet.MapManager/Data/Action.cs create mode 100644 RobotNet.MapManager/Data/Edge.cs create mode 100644 RobotNet.MapManager/Data/Element.cs create mode 100644 RobotNet.MapManager/Data/ElementModel.cs create mode 100644 RobotNet.MapManager/Data/Map.cs create mode 100644 RobotNet.MapManager/Data/MapEditorDbContext.cs create mode 100644 RobotNet.MapManager/Data/MapManagerDbExtensions.cs create mode 100644 RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.Designer.cs create mode 100644 RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.cs create mode 100644 RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.Designer.cs create mode 100644 RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.cs create mode 100644 RobotNet.MapManager/Data/Migrations/MapEditorDbContextModelSnapshot.cs create mode 100644 RobotNet.MapManager/Data/Node.cs create mode 100644 RobotNet.MapManager/Data/Zone.cs create mode 100644 RobotNet.MapManager/Dockerfile create mode 100644 RobotNet.MapManager/Hubs/MapHub.cs create mode 100644 RobotNet.MapManager/Program.cs create mode 100644 RobotNet.MapManager/Properties/launchSettings.json create mode 100644 RobotNet.MapManager/RobotNet.MapManager.csproj create mode 100644 RobotNet.MapManager/RobotNet.MapManager.http create mode 100644 RobotNet.MapManager/Services/LoggerController.cs create mode 100644 RobotNet.MapManager/Services/MapEditorStorageRepository.cs create mode 100644 RobotNet.MapManager/Services/ServerHelper.cs create mode 100644 RobotNet.MapManager/appsettings.json create mode 100644 RobotNet.MapManager/nlog.config create mode 100644 RobotNet.MapShares/Dtos/ActionDto.cs create mode 100644 RobotNet.MapShares/Dtos/EdgeDto.cs create mode 100644 RobotNet.MapShares/Dtos/ElementDto.cs create mode 100644 RobotNet.MapShares/Dtos/ElementModelDto.cs create mode 100644 RobotNet.MapShares/Dtos/MapDataExportDto.cs create mode 100644 RobotNet.MapShares/Dtos/MapDto.cs create mode 100644 RobotNet.MapShares/Dtos/NodeDto.cs create mode 100644 RobotNet.MapShares/Dtos/ZoneDto.cs create mode 100644 RobotNet.MapShares/Enums/AlignState.cs create mode 100644 RobotNet.MapShares/Enums/BlockingType.cs create mode 100644 RobotNet.MapShares/Enums/ControlState.cs create mode 100644 RobotNet.MapShares/Enums/Direction.cs create mode 100644 RobotNet.MapShares/Enums/DirectionAllowed.cs create mode 100644 RobotNet.MapShares/Enums/EditorState.cs create mode 100644 RobotNet.MapShares/Enums/TrajectoryDegree.cs create mode 100644 RobotNet.MapShares/Enums/ZoneType.cs create mode 100644 RobotNet.MapShares/JsonOptionExtends.cs create mode 100644 RobotNet.MapShares/MapEditorHelper.cs create mode 100644 RobotNet.MapShares/MapManagerExtensions.cs create mode 100644 RobotNet.MapShares/Models/ElementExpressionModel.cs create mode 100644 RobotNet.MapShares/Models/LoggerModel.cs create mode 100644 RobotNet.MapShares/Models/MapEditorBackupModel.cs create mode 100644 RobotNet.MapShares/Property/ElementProperty.cs create mode 100644 RobotNet.MapShares/RobotNet.MapShares.csproj create mode 100644 RobotNet.OpenIddictClient/OpenIddictClientProviderOptions.cs create mode 100644 RobotNet.OpenIddictClient/OpenIddictResourceOptions.cs create mode 100644 RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj create mode 100644 RobotNet.RobotManager/Controllers/OpenACSSettingsController.cs create mode 100644 RobotNet.RobotManager/Controllers/RobotManagerController.cs create mode 100644 RobotNet.RobotManager/Controllers/RobotManagerLoggerController.cs create mode 100644 RobotNet.RobotManager/Controllers/RobotModelsController.cs create mode 100644 RobotNet.RobotManager/Controllers/RobotsController.cs create mode 100644 RobotNet.RobotManager/Controllers/TrafficACSRequestController.cs create mode 100644 RobotNet.RobotManager/Controllers/TrafficManagerController.cs create mode 100644 RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.Designer.cs create mode 100644 RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.cs create mode 100644 RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.Designer.cs create mode 100644 RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.cs create mode 100644 RobotNet.RobotManager/Data/Migrations/RobotEditorDbContextModelSnapshot.cs create mode 100644 RobotNet.RobotManager/Data/Robot.cs create mode 100644 RobotNet.RobotManager/Data/RobotEditorDbContext.cs create mode 100644 RobotNet.RobotManager/Data/RobotManagerDbExtensions.cs create mode 100644 RobotNet.RobotManager/Data/RobotModel.cs create mode 100644 RobotNet.RobotManager/Dockerfile create mode 100644 RobotNet.RobotManager/HubClients/MapHubClient.cs create mode 100644 RobotNet.RobotManager/HubClients/OpenIddictHubClient.cs create mode 100644 RobotNet.RobotManager/Hubs/RobotHub.cs create mode 100644 RobotNet.RobotManager/Hubs/RobotManagerHub.cs create mode 100644 RobotNet.RobotManager/Hubs/TrafficHub.cs create mode 100644 RobotNet.RobotManager/Program.cs create mode 100644 RobotNet.RobotManager/Properties/launchSettings.json create mode 100644 RobotNet.RobotManager/RobotNet.RobotManager.csproj create mode 100644 RobotNet.RobotManager/RobotNet.RobotManager.http create mode 100644 RobotNet.RobotManager/Services/IRobotController.cs create mode 100644 RobotNet.RobotManager/Services/IRobotOrder.cs create mode 100644 RobotNet.RobotManager/Services/JsonOptionExtends.cs create mode 100644 RobotNet.RobotManager/Services/LoggerController.cs create mode 100644 RobotNet.RobotManager/Services/MapManager.cs create mode 100644 RobotNet.RobotManager/Services/MapManagerAccessTokenHandler.cs create mode 100644 RobotNet.RobotManager/Services/MqttBroker.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/ACSHeader.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/ACSStatusResponse.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/AGVLocation.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/AGVState.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/OpenACSException.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/OpenACSManager.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/OpenACSPublisher.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/RobotPublishStatus.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/RobotPublishStatusBody.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/TrafficACS.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/TrafficACSRequest.cs create mode 100644 RobotNet.RobotManager/Services/OpenACS/TrafficACSResponse.cs create mode 100644 RobotNet.RobotManager/Services/PathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/AStar/AStarNode.cs create mode 100644 RobotNet.RobotManager/Services/Planner/AStar/AStarPathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Differential/DifferentialPathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Fokrlift/ForkliftPathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Fokrlift/TStructure.cs create mode 100644 RobotNet.RobotManager/Services/Planner/ForkliftV2/ForkLiftPathPlannerV2.cs create mode 100644 RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarNode.cs create mode 100644 RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarPathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/IPathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/IPathPlannerManager.cs create mode 100644 RobotNet.RobotManager/Services/Planner/OmniDrive/OmniDrivePathPlanner.cs create mode 100644 RobotNet.RobotManager/Services/Planner/PathPlannerManager.cs create mode 100644 RobotNet.RobotManager/Services/Planner/PathPlanningOptions.cs create mode 100644 RobotNet.RobotManager/Services/Planner/PriorityQueue.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Space/KDTree.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Space/MapCompute.cs create mode 100644 RobotNet.RobotManager/Services/Planner/Space/RTree.cs create mode 100644 RobotNet.RobotManager/Services/Robot/RobotController.cs create mode 100644 RobotNet.RobotManager/Services/Robot/RobotOrder.cs create mode 100644 RobotNet.RobotManager/Services/RobotEditorStorageRepository.cs create mode 100644 RobotNet.RobotManager/Services/RobotManager.cs create mode 100644 RobotNet.RobotManager/Services/RobotPublisher.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Algorithm/FuzzyLogic.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Algorithm/MathExtension.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Algorithm/PID.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Algorithm/PurePursuit.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/DifferentialNavigationService.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/ForkliftNavigationSevice.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/GridDifferentialNavigationService.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/INavigationService.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Models/NavigationAction.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Models/NavigationNode.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Models/NavigationStateType.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/Models/RobotSimulationModel.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/NavigationManager.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/NavigationService.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/RobotSimulation.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/VelocityController.cs create mode 100644 RobotNet.RobotManager/Services/Simulation/VisualizationService.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/Agent.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/AgentModel.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficAlarm.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficConflict.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficEdgeDto.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficGiveway.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficManager.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficMap.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficMath.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficPublisher.cs create mode 100644 RobotNet.RobotManager/Services/Traffic/TrafficSolution.cs create mode 100644 RobotNet.RobotManager/Services/WatchTimer.cs create mode 100644 RobotNet.RobotManager/Services/WatchTimerAsync.cs create mode 100644 RobotNet.RobotManager/appsettings.json create mode 100644 RobotNet.RobotManager/nlog.config create mode 100644 RobotNet.RobotShares/Dtos/RobotActionDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotInfomationDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotModelDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotOnlineStateDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotOrderDto.cs create mode 100644 RobotNet.RobotShares/Dtos/RobotVDA5050StateDto.cs create mode 100644 RobotNet.RobotShares/Dtos/TrafficAgentDto.cs create mode 100644 RobotNet.RobotShares/Dtos/TrafficLockedShapeDto.cs create mode 100644 RobotNet.RobotShares/Dtos/TrafficMapDto.cs create mode 100644 RobotNet.RobotShares/Dtos/TrafficNodeDto.cs create mode 100644 RobotNet.RobotShares/Enums/MonitorToolbarButtonType.cs create mode 100644 RobotNet.RobotShares/Enums/MonitorToolbarCheckedType.cs create mode 100644 RobotNet.RobotShares/Enums/NavigationType.cs create mode 100644 RobotNet.RobotShares/Enums/RefreshPathState.cs create mode 100644 RobotNet.RobotShares/Enums/RobotDirection.cs create mode 100644 RobotNet.RobotShares/Enums/TrafficConflictState.cs create mode 100644 RobotNet.RobotShares/Enums/TrafficSolutionState.cs create mode 100644 RobotNet.RobotShares/Models/RobotInstantActionModel.cs create mode 100644 RobotNet.RobotShares/Models/RobotMoveStraightModel.cs create mode 100644 RobotNet.RobotShares/Models/RobotMoveToNodeModel.cs create mode 100644 RobotNet.RobotShares/Models/RobotRotateModel.cs create mode 100644 RobotNet.RobotShares/Models/RobotSearchExpressionModel.cs create mode 100644 RobotNet.RobotShares/Models/RobotStateModel.cs create mode 100644 RobotNet.RobotShares/Models/UpdateAgentLockerModel.cs create mode 100644 RobotNet.RobotShares/OpenACS/OpenACSPublishSettingDto.cs create mode 100644 RobotNet.RobotShares/OpenACS/OpenACSSettingsDto.cs create mode 100644 RobotNet.RobotShares/OpenACS/OpenACSTrafficSettingDto.cs create mode 100644 RobotNet.RobotShares/OpenACS/RobotACSLockedDto.cs create mode 100644 RobotNet.RobotShares/OpenACS/TrafficACSRequestModel.cs create mode 100644 RobotNet.RobotShares/OpenACS/TrafficRequestType.cs create mode 100644 RobotNet.RobotShares/RobotNet.RobotShares.csproj create mode 100644 RobotNet.RobotShares/VDA5050/Connection/ConnectionMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/ActionParameters.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/AgvActions.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/AgvGeometry.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/BoundingBoxReference.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/Envelopes2d.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/Envelopes3d.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/FactSheetMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/LoadDimensions.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/LoadSets.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/LoadSpecification.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/LocalizationParameter.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/MaxArrayLens.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/MaxStringLens.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/OptionalParameters.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/PhysicalParameters.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/ProtocolFeatures.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/ProtocolLimits.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/Timing.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/TypeSpecification.cs create mode 100644 RobotNet.RobotShares/VDA5050/Factsheet/WheelDefinitions.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Battery.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/BatteryThreshold.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/CameraSafety.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/ChargerParam.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/FactsheetExtendMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/ForkSafety.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Initpose.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/LineSegment.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Localization.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Motor.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Navigation.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/PPA.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/PTA.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/RobotParam.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Rotate.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Safety.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/ServerParam.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/VlMarker.cs create mode 100644 RobotNet.RobotShares/VDA5050/FactsheetExtend/Xloc.cs create mode 100644 RobotNet.RobotShares/VDA5050/InstantAction/ActionParameter.cs create mode 100644 RobotNet.RobotShares/VDA5050/InstantAction/Actions.cs create mode 100644 RobotNet.RobotShares/VDA5050/InstantAction/InstantActionsMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/Corridor.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/Edge.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/EdgeLog.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/Node.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/NodeLog.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/NodePosition.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/OrderLog.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/OrderMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/Order/Trajectory.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/ActionState.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/BatteryState.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/EdgeState.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/Error.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/Information.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/Load.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/Map.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/NodeState.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/SafetyState.cs create mode 100644 RobotNet.RobotShares/VDA5050/State/StateMsg.cs create mode 100644 RobotNet.RobotShares/VDA5050/Type/ActionType.cs create mode 100644 RobotNet.RobotShares/VDA5050/Type/InformationType.cs create mode 100644 RobotNet.RobotShares/VDA5050/Type/LoadType.cs create mode 100644 RobotNet.RobotShares/VDA5050/Type/ManualActionType.cs create mode 100644 RobotNet.RobotShares/VDA5050/VDA5050Helper.cs create mode 100644 RobotNet.RobotShares/VDA5050/VDA5050Setting.cs create mode 100644 RobotNet.RobotShares/VDA5050/VDA5050Topic.cs create mode 100644 RobotNet.RobotShares/VDA5050/Visualization/AgvPosition.cs create mode 100644 RobotNet.RobotShares/VDA5050/Visualization/Velocity.cs create mode 100644 RobotNet.RobotShares/VDA5050/Visualization/Visualizationmsg.cs create mode 100644 RobotNet.Script.Expressions/ElementProperties.cs create mode 100644 RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj create mode 100644 RobotNet.Script.Expressions/RobotState.cs create mode 100644 RobotNet.Script.Shares/CreateModel.cs create mode 100644 RobotNet.Script.Shares/Dashboard/DailyMissionDto.cs create mode 100644 RobotNet.Script.Shares/Dashboard/DailyPerformanceDto.cs create mode 100644 RobotNet.Script.Shares/Dashboard/DashboardDto.cs create mode 100644 RobotNet.Script.Shares/Dashboard/TaktTimeMissionDto.cs create mode 100644 RobotNet.Script.Shares/IRobotNetGlobals.cs create mode 100644 RobotNet.Script.Shares/InstanceMissionCreateModel.cs create mode 100644 RobotNet.Script.Shares/InstanceMissionDto.cs create mode 100644 RobotNet.Script.Shares/MissionStatus.cs create mode 100644 RobotNet.Script.Shares/ProcessorRequest.cs create mode 100644 RobotNet.Script.Shares/ProcessorState.cs create mode 100644 RobotNet.Script.Shares/RenameModel.cs create mode 100644 RobotNet.Script.Shares/RobotNet.Script.Shares.csproj create mode 100644 RobotNet.Script.Shares/ScriptExtensions.cs create mode 100644 RobotNet.Script.Shares/ScriptFile.cs create mode 100644 RobotNet.Script.Shares/ScriptFolder.cs create mode 100644 RobotNet.Script.Shares/ScriptMissionDto.cs create mode 100644 RobotNet.Script.Shares/ScriptTaskDto.cs create mode 100644 RobotNet.Script.Shares/ScriptVariableDto.cs create mode 100644 RobotNet.Script.Shares/UpdateModel.cs create mode 100644 RobotNet.Script.Shares/UpdateVariableModel.cs create mode 100644 RobotNet.Script/ICcLinkIeBasicClient.cs create mode 100644 RobotNet.Script/IConnectionManager.cs create mode 100644 RobotNet.Script/ILogger.cs create mode 100644 RobotNet.Script/IMapManager.cs create mode 100644 RobotNet.Script/IModbusTcpClient.cs create mode 100644 RobotNet.Script/IRobot.cs create mode 100644 RobotNet.Script/IUnixDevice.cs create mode 100644 RobotNet.Script/MissionAttribute.cs create mode 100644 RobotNet.Script/RobotNet.Script.csproj create mode 100644 RobotNet.Script/TaskAttribute.cs create mode 100644 RobotNet.Script/VariableAttribute.cs create mode 100644 RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs create mode 100644 RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs create mode 100644 RobotNet.ScriptManager/Connections/ModbusTcpClient.cs create mode 100644 RobotNet.ScriptManager/Connections/UnixDevice.cs create mode 100644 RobotNet.ScriptManager/Controllers/DashboardConfigController.cs create mode 100644 RobotNet.ScriptManager/Controllers/ScriptController.cs create mode 100644 RobotNet.ScriptManager/Controllers/ScriptManagerLoggerController.cs create mode 100644 RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs create mode 100644 RobotNet.ScriptManager/Controllers/ScriptTasksController.cs create mode 100644 RobotNet.ScriptManager/Controllers/ScriptVariablesController.cs create mode 100644 RobotNet.ScriptManager/Data/InstanceMission.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.cs create mode 100644 RobotNet.ScriptManager/Data/Migrations/ScriptManagerDbContextModelSnapshot.cs create mode 100644 RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs create mode 100644 RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs create mode 100644 RobotNet.ScriptManager/Dockerfile create mode 100644 RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs create mode 100644 RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs create mode 100644 RobotNet.ScriptManager/Helpers/VDA5050Helper.cs create mode 100644 RobotNet.ScriptManager/Hubs/ConsoleHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/DashboardHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/HMIHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/ProcessorHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs create mode 100644 RobotNet.ScriptManager/Hubs/VariablesHub.cs create mode 100644 RobotNet.ScriptManager/Models/ConsoleLog.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMapElement.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMapManager.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMapNode.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMission.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMissionData.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptMissionLogger.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptRobotManager.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptTask.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptTaskData.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptTaskLogger.cs create mode 100644 RobotNet.ScriptManager/Models/ScriptVariable.cs create mode 100644 RobotNet.ScriptManager/Program.cs create mode 100644 RobotNet.ScriptManager/Properties/launchSettings.json create mode 100644 RobotNet.ScriptManager/RobotNet.ScriptManager.csproj create mode 100644 RobotNet.ScriptManager/Services/DashboardConfig.cs create mode 100644 RobotNet.ScriptManager/Services/DashboardPublisher.cs create mode 100644 RobotNet.ScriptManager/Services/LoopService.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptConnectionManager.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptMissionCreator.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptMissionManager.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptStateManager.cs create mode 100644 RobotNet.ScriptManager/Services/ScriptTaskManager.cs create mode 100644 RobotNet.ScriptManager/appsettings.json create mode 100644 RobotNet.ScriptManager/nlog.config create mode 100644 RobotNet.ScriptManager/readme create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.Expressions.dll create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.Expressions.xml create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.dll create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/System.Collections.dll create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/System.Linq.Expressions.dll create mode 100644 RobotNet.ScriptManager/wwwroot/dlls/System.Runtime.dll create mode 100644 RobotNet.ServiceDefaults/Dockerfile create mode 100644 RobotNet.ServiceDefaults/Extensions.cs create mode 100644 RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj create mode 100644 RobotNet.Shares/MessageResult.cs create mode 100644 RobotNet.Shares/RobotNet.Shares.csproj create mode 100644 RobotNet.Shares/SearchResult.cs create mode 100644 RobotNet.SystemUpgrade/AppJsonSerializerContext.cs create mode 100644 RobotNet.SystemUpgrade/Program.cs create mode 100644 RobotNet.SystemUpgrade/Properties/launchSettings.json create mode 100644 RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj create mode 100644 RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln create mode 100644 RobotNet.SystemUpgrade/appsettings.Development.json create mode 100644 RobotNet.SystemUpgrade/appsettings.json create mode 100644 RobotNet.SystemUpgrade/libman.json create mode 100644 RobotNet.SystemUpgrade/readme.md create mode 100644 RobotNet.SystemUpgrade/wwwroot/favicon.svg create mode 100644 RobotNet.SystemUpgrade/wwwroot/index.html create mode 100644 RobotNet.WebApp/App.razor create mode 100644 RobotNet.WebApp/Charts/Components/BarChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/BarChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/BubbleChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/BubbleChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/LineChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/LineChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/PieChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/PieChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/PolarAreaChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/PolarAreaChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/RadarChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/RadarChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Components/ScatterChart.razor create mode 100644 RobotNet.WebApp/Charts/Components/ScatterChart.razor.cs create mode 100644 RobotNet.WebApp/Charts/Core/BlazorComponentBase.cs create mode 100644 RobotNet.WebApp/Charts/Core/ChartColors.cs create mode 100644 RobotNet.WebApp/Charts/Core/RobotNetChart.cs create mode 100644 RobotNet.WebApp/Charts/Enums/AxisDirection.cs create mode 100644 RobotNet.WebApp/Charts/Enums/BorderAlign.cs create mode 100644 RobotNet.WebApp/Charts/Enums/BorderJoinStyle.cs create mode 100644 RobotNet.WebApp/Charts/Enums/BorderSkipped.cs create mode 100644 RobotNet.WebApp/Charts/Enums/ChartAxesType.cs create mode 100644 RobotNet.WebApp/Charts/Enums/ChartType.cs create mode 100644 RobotNet.WebApp/Charts/Enums/DataLabelsAlignment.cs create mode 100644 RobotNet.WebApp/Charts/Enums/DataLabelsAnchoring.cs create mode 100644 RobotNet.WebApp/Charts/Enums/DatasetType.cs create mode 100644 RobotNet.WebApp/Charts/Enums/Easing.cs create mode 100644 RobotNet.WebApp/Charts/Enums/EnumExtensions.cs create mode 100644 RobotNet.WebApp/Charts/Enums/IndexAxis.cs create mode 100644 RobotNet.WebApp/Charts/Enums/InteractionMode.cs create mode 100644 RobotNet.WebApp/Charts/Enums/LegendAlignment.cs create mode 100644 RobotNet.WebApp/Charts/Enums/LegendPosition.cs create mode 100644 RobotNet.WebApp/Charts/Enums/LowerCaseEnumConverter.cs create mode 100644 RobotNet.WebApp/Charts/Enums/PointStyle.cs create mode 100644 RobotNet.WebApp/Charts/Enums/TicksAlignment.cs create mode 100644 RobotNet.WebApp/Charts/Enums/TitleAlignment.cs create mode 100644 RobotNet.WebApp/Charts/Enums/TitlePosition.cs create mode 100644 RobotNet.WebApp/Charts/Enums/TooltipAlignment.cs create mode 100644 RobotNet.WebApp/Charts/Enums/TooltipPosition.cs create mode 100644 RobotNet.WebApp/Charts/Enums/Unit.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxes.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesBorder.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesGrid.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicks.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicksMajor.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTitle.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/BarChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/BarChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/BarChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/BarChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/BarChart/BarScales.cs create mode 100644 RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/BubbleChart/BubblePoint.cs create mode 100644 RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLinePlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineScales.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ArcAnimation.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ChartAnimation.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ChartFont.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ChartLayout.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/ChartPadding.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Dataset/ChartData.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetDataLabels.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Elements/Arc.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Elements/Bar.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Elements/ChartElements.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Elements/Line.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Elements/Point.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/IChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/IChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/IChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Interaction.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsDataLabels.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegend.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendLabels.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendTitle.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsSubtitle.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTitle.cs create mode 100644 RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTooltip.cs create mode 100644 RobotNet.WebApp/Charts/Models/LineChart/LineChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/LineChart/LineChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/LineChart/LineChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/LineChart/LineChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/PieChart/PieChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/PieChart/PieChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/PieChart/PieChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/PieChart/PieChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/AngleLines.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/GridLines.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/PointLabels.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarAxis.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarScale.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadialTicks.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/RadarChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/RadarChart/RadarChartPlugins.cs create mode 100644 RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataPoint.cs create mode 100644 RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataset.cs create mode 100644 RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDatasetData.cs create mode 100644 RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartOptions.cs create mode 100644 RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartPlugins.cs create mode 100644 RobotNet.WebApp/Clients/ConsoleHubClient.cs create mode 100644 RobotNet.WebApp/Clients/DashboardHubClient.cs create mode 100644 RobotNet.WebApp/Clients/HMIHubClient.cs create mode 100644 RobotNet.WebApp/Clients/ProcessorHubClient.cs create mode 100644 RobotNet.WebApp/Clients/RobotHubClient.cs create mode 100644 RobotNet.WebApp/Clients/ScriptTaskHubClient.cs create mode 100644 RobotNet.WebApp/Clients/TrafficHubClient.cs create mode 100644 RobotNet.WebApp/Clients/WebAssemblyHubClient.cs create mode 100644 RobotNet.WebApp/Components/ConfirmDialog.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor.css create mode 100644 RobotNet.WebApp/Dashboard/Components/DailyData.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor.css create mode 100644 RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor.css create mode 100644 RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor create mode 100644 RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor.css create mode 100644 RobotNet.WebApp/Dockerfile create mode 100644 RobotNet.WebApp/Helpers/DefaultPersistentStorageConfigurationExtensions.cs create mode 100644 RobotNet.WebApp/Helpers/ServiceCollectionExtensions.cs create mode 100644 RobotNet.WebApp/Layout/HMILayout.razor create mode 100644 RobotNet.WebApp/Layout/MainLayout.razor create mode 100644 RobotNet.WebApp/Layout/NavMenu.razor create mode 100644 RobotNet.WebApp/Layout/NavMenu.razor.css create mode 100644 RobotNet.WebApp/Layout/RedirectToLogin.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.cs create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapCopy.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/MapZone.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor create mode 100644 RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementDefaultProperty.razor create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementImage.razor create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementImage.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Element/ElementPropertyTable.razor create mode 100644 RobotNet.WebApp/Maps/Components/Element/MapElement.razor create mode 100644 RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor create mode 100644 RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor create mode 100644 RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor create mode 100644 RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor.css create mode 100644 RobotNet.WebApp/Maps/Components/Toolbar/EditorToolbar.razor create mode 100644 RobotNet.WebApp/Maps/Components/_Imports.razor create mode 100644 RobotNet.WebApp/Maps/Models/EdgeModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/ElementModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/MapEdgeModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/MapElementModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/MapNodeModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/MapZoneModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/NodeModel.cs create mode 100644 RobotNet.WebApp/Maps/Models/ZoneModel.cs create mode 100644 RobotNet.WebApp/Pages/Authentication.razor create mode 100644 RobotNet.WebApp/Pages/Dashboard.razor create mode 100644 RobotNet.WebApp/Pages/Dashboard.razor.css create mode 100644 RobotNet.WebApp/Pages/Logs.razor create mode 100644 RobotNet.WebApp/Pages/Logs.razor.css create mode 100644 RobotNet.WebApp/Pages/Missions.razor create mode 100644 RobotNet.WebApp/Pages/NavigationMapEditor.razor create mode 100644 RobotNet.WebApp/Pages/NavigationMapElement.razor create mode 100644 RobotNet.WebApp/Pages/NavigationMapSetting.razor create mode 100644 RobotNet.WebApp/Pages/NavigationMapsManager.razor create mode 100644 RobotNet.WebApp/Pages/NavigationMapsManager.razor.css create mode 100644 RobotNet.WebApp/Pages/OpenACSSettings.razor create mode 100644 RobotNet.WebApp/Pages/RobotDetail.razor create mode 100644 RobotNet.WebApp/Pages/RobotManager.razor create mode 100644 RobotNet.WebApp/Pages/RobotModelManager.razor create mode 100644 RobotNet.WebApp/Pages/RobotModelManager.razor.css create mode 100644 RobotNet.WebApp/Pages/RobotMonitoring.razor create mode 100644 RobotNet.WebApp/Pages/RobotTrafficManager.razor create mode 100644 RobotNet.WebApp/Pages/RobotTrafficManager.razor.css create mode 100644 RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor create mode 100644 RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.cs create mode 100644 RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.css create mode 100644 RobotNet.WebApp/Pages/ScriptEditor.razor create mode 100644 RobotNet.WebApp/Pages/ScriptEditor.razor.css create mode 100644 RobotNet.WebApp/Pages/ScriptManager.razor create mode 100644 RobotNet.WebApp/Pages/User.razor create mode 100644 RobotNet.WebApp/Program.cs create mode 100644 RobotNet.WebApp/Properties/launchSettings.json create mode 100644 RobotNet.WebApp/RobotNet.WebApp.csproj create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MonitorInfomation.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor.cs create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/RobotCurrentPath.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/RobotLaserScaner.razor create mode 100644 RobotNet.WebApp/Robots/Components/Monitoring/RobotPath.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/DashboardConfig.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/PublishSetting.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/TrafficRequest.razor create mode 100644 RobotNet.WebApp/Robots/Components/OpenACS/TrafficSetting.razor create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotAction.razor create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor create mode 100644 RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor.css create mode 100644 RobotNet.WebApp/Robots/Components/Traffic/TrafficAgentReview.razor create mode 100644 RobotNet.WebApp/Robots/Components/Traffic/TrafficNodePreview.razor create mode 100644 RobotNet.WebApp/Robots/Components/_Imports.razor create mode 100644 RobotNet.WebApp/Scripts/Components/ActionButton.razor create mode 100644 RobotNet.WebApp/Scripts/Components/ActionButton.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/ConsoleItem.razor create mode 100644 RobotNet.WebApp/Scripts/Components/ConsoleItem.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/InstantiateMissionDialog.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/Missions.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/ProcessController.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/SetVariableValueDialog.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/Tasks.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dashboards/Variables.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dialogs/CreateNewFileOrFolderDialog.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dialogs/DeleteFileOrFolderDialog.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Dialogs/RenameFileOrFolderDialog.razor create mode 100644 RobotNet.WebApp/Scripts/Components/Editor.razor create mode 100644 RobotNet.WebApp/Scripts/Components/EditorHierachy.razor create mode 100644 RobotNet.WebApp/Scripts/Components/EditorHierachy.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/FileItem.razor create mode 100644 RobotNet.WebApp/Scripts/Components/FileItem.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/FolderItem.razor create mode 100644 RobotNet.WebApp/Scripts/Components/FolderItem.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemName.razor create mode 100644 RobotNet.WebApp/Scripts/Components/HierachyItemName.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/Loading.razor create mode 100644 RobotNet.WebApp/Scripts/Components/ProcessorConsole.razor create mode 100644 RobotNet.WebApp/Scripts/Components/ProcessorControl.razor create mode 100644 RobotNet.WebApp/Scripts/Components/SidebarActions.razor create mode 100644 RobotNet.WebApp/Scripts/Components/SidebarActions.razor.css create mode 100644 RobotNet.WebApp/Scripts/Components/_Imports.razor create mode 100644 RobotNet.WebApp/Scripts/Models/ConsoleItemModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/HierachyItemModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/InstanceMissionModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptFileModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptFolderModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptMissionModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptMissionParameterModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptTaskModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptVariableModel.cs create mode 100644 RobotNet.WebApp/Scripts/Models/ScriptWorkspace.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationComment.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationConverter.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationItem.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Editor/MarkerData.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChange.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChangedEvent.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Editor/RelatedInformation.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Editor/SingleEditOperation.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/InvocationContext.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/Command.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionExtensions.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItem.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemInsertTextRule.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemKind.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemLabel.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemRanges.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemTag.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/CompletionList.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/ParameterInformation.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelp.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpExtensions.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpResult.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Languages/SignatureInformation.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/MarkdownHelpers.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/MarkdownString.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/MarkerSeverity.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/MarkerTag.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/MonacoExtensions.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/Range.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/StringBuilderExtension.cs create mode 100644 RobotNet.WebApp/Scripts/Monaco/UriComponents.cs create mode 100644 RobotNet.WebApp/_Imports.razor create mode 100644 RobotNet.WebApp/libman.json create mode 100644 RobotNet.WebApp/nginx.conf create mode 100644 RobotNet.WebApp/wwwroot/appsettings.json create mode 100644 RobotNet.WebApp/wwwroot/css/app.css create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAo.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmYiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmZiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVn6iArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 create mode 100644 RobotNet.WebApp/wwwroot/css/mud/fonts.googleapis.com.css create mode 100644 RobotNet.WebApp/wwwroot/favicon.svg create mode 100644 RobotNet.WebApp/wwwroot/icon-192.png create mode 100644 RobotNet.WebApp/wwwroot/icon-512.png create mode 100644 RobotNet.WebApp/wwwroot/images/Image-not-found.png create mode 100644 RobotNet.WebApp/wwwroot/images/logoDark.svg create mode 100644 RobotNet.WebApp/wwwroot/images/logoLight.svg create mode 100644 RobotNet.WebApp/wwwroot/index.html create mode 100644 RobotNet.WebApp/wwwroot/js/app.js create mode 100644 RobotNet.WebApp/wwwroot/js/chart.umd.js create mode 100644 RobotNet.WebApp/wwwroot/js/chart.umd.js.map create mode 100644 RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.esm.js create mode 100644 RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.js create mode 100644 RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.min.js create mode 100644 RobotNet.WebApp/wwwroot/js/map.js create mode 100644 RobotNet.WebApp/wwwroot/js/robonet.chart.js create mode 100644 RobotNet.WebApp/wwwroot/js/robot-design.js create mode 100644 RobotNet.WebApp/wwwroot/js/script.js create mode 100644 RobotNet.WebApp/wwwroot/manifest.webmanifest create mode 100644 RobotNet.WebApp/wwwroot/sehc/amr.png create mode 100644 RobotNet.WebApp/wwwroot/sehc/station.png create mode 100644 RobotNet.WebApp/wwwroot/sehc/trolley.png create mode 100644 RobotNet.WebApp/wwwroot/service-worker.js create mode 100644 RobotNet.WebApp/wwwroot/service-worker.published.js create mode 100644 RobotNet.sln create mode 100644 appsettings.RobotNet.WebApp.json create mode 100644 certificate/gencert.cmd create mode 100644 certificate/gencert.sh create mode 100644 certificate/san.cnf create mode 100644 clean.ps1 create mode 100644 docker-compose.yaml create mode 100644 docker-deploy.yaml create mode 100644 install.md create mode 100644 sehcio/Makefile create mode 100644 sehcio/sehcio.c diff --git a/.env b/.env new file mode 100644 index 0000000..6dae422 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +DOCKER_HUB=robotics.doc/robotnet +TAG=0.12.0 +HOST_IP=172.20.235.172 +CERT_PASSWORD=RobotNet@2024 +SQL_IP=172.20.235.170 +SQL_PASSWORD=robotics@2022 +SQL_IDENTITY_DB=IdentityDb +SQL_MAP_MANAGER_DB=MapDb +SQL_ROBOT_MANAGER_DB=RobotDb +SQL_SCRIPT_MANAGER_DB=ScriptDb +MINIO_IP=172.20.235.170 +MINIO_ROOT_USER=minio +MINIO_ROOT_PASSWORD=robotics +IDENTITY_SERVER_PORT=8061 +MAP_MANAGER_PORT=8177 +ROBOT_MANAGER_PORT=8179 +MQTT_PORT=1883 +SCRIPT_MANAGER_PORT=8102 +WEB_APP_PORT=8035 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ab217c --- /dev/null +++ b/.gitignore @@ -0,0 +1,409 @@ +# ---> VisualStudio +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +*/wwwroot/lib + +certificate/*.pfx +certificate/*.crt +certificate/*.key +certificate/*.pem +certificate/*.srl +certificate/*.csr +.scripts/ \ No newline at end of file diff --git a/RobotNet.AppHost/Dockerfile b/RobotNet.AppHost/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/RobotNet.AppHost/Program.cs b/RobotNet.AppHost/Program.cs new file mode 100644 index 0000000..269438a --- /dev/null +++ b/RobotNet.AppHost/Program.cs @@ -0,0 +1,48 @@ +var builder = DistributedApplication.CreateBuilder(args); + +//var cache = builder.AddRedis("cache"); + +var identity = builder.AddProject("identity-server") + .WithExternalHttpEndpoints(); +//.WithReference(cache) +//.WaitFor(cache); + +var mapManager = builder.AddProject("map-manager") + .WithExternalHttpEndpoints() + //.WithReference(cache) + //.WaitFor(cache) + .WithReference(identity) + .WaitFor(identity); + +var robotManager = builder.AddProject("robot-manager") + .WithExternalHttpEndpoints() + //.WithReference(cache) + //.WaitFor(cache) + .WithReference(identity) + .WaitFor(identity) + .WithReference(mapManager) + .WaitFor(mapManager); + +var scriptManager = builder.AddProject("script-manager") + .WithExternalHttpEndpoints() + //.WithReference(cache) + //.WaitFor(cache) + .WithReference(identity) + .WaitFor(identity) + .WithReference(robotManager) + .WaitFor(robotManager); + +builder.AddProject("robotnet-webapp") + .WithExternalHttpEndpoints() + //.WithReference(cache) + //.WaitFor(cache) + .WithReference(identity) + .WaitFor(identity) + .WithReference(mapManager) + .WaitFor(mapManager) + .WithReference(robotManager) + .WaitFor(robotManager) + .WithReference(scriptManager) + .WaitFor(scriptManager); + +builder.Build().Run(); diff --git a/RobotNet.AppHost/Properties/launchSettings.json b/RobotNet.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..d15e289 --- /dev/null +++ b/RobotNet.AppHost/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21061", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22197" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17070;http://localhost:15043", + "remoteDebugEnabled": false, + "authenticationMode": "None" + }, + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19047", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20292" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:15043" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/RobotNet.AppHost/RobotNet.AppHost.csproj b/RobotNet.AppHost/RobotNet.AppHost.csproj new file mode 100644 index 0000000..4710ade --- /dev/null +++ b/RobotNet.AppHost/RobotNet.AppHost.csproj @@ -0,0 +1,27 @@ + + + + + + Exe + net9.0 + enable + enable + true + ee4f8e12-ccfe-4b55-94bb-c86fe3a6b387 + + + + + + + + + + + + + + + + diff --git a/RobotNet.AppHost/appsettings.Development.json b/RobotNet.AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RobotNet.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RobotNet.AppHost/appsettings.json b/RobotNet.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/RobotNet.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/RobotNet.Clients/HttpClientExtensions.cs b/RobotNet.Clients/HttpClientExtensions.cs new file mode 100644 index 0000000..b9b9ca3 --- /dev/null +++ b/RobotNet.Clients/HttpClientExtensions.cs @@ -0,0 +1,36 @@ +using System.Net.Http.Json; + +namespace RobotNet.Clients; + +public static class HttpClientExtensions +{ + public static async Task PostFromJsonAsync(this HttpClient client, string requestUri, object value) + { + var response = await client.PostAsJsonAsync(requestUri, value); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + return default; + } + + public static async Task PutFromJsonAsync(this HttpClient client, string requestUri, object value) + { + var response = await client.PutAsJsonAsync(requestUri, value); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + return default; + } + + public static async Task PatchFromJsonAsync(this HttpClient client, string requestUri, object value) + { + var response = await client.PatchAsJsonAsync(requestUri, value); + if (response.IsSuccessStatusCode) + { + return await response.Content.ReadFromJsonAsync(); + } + return default; + } +} diff --git a/RobotNet.Clients/HubClient.cs b/RobotNet.Clients/HubClient.cs new file mode 100644 index 0000000..87c57d3 --- /dev/null +++ b/RobotNet.Clients/HubClient.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.SignalR.Client; + +namespace RobotNet.Clients; + +public abstract class HubClient +{ + public event Action? ConnectionStateChanged; + public bool IsConnected => Connection.State == HubConnectionState.Connected; + protected HubConnection Connection { get; } + + protected HubClient(Uri url, Func> accessTokenProvider) + { + Connection = new HubConnectionBuilder() + .WithUrl(url, options => + { + options.AccessTokenProvider = accessTokenProvider; + }) + .WithAutomaticReconnect(new HubClientRepeatRetryPolicy(TimeSpan.FromSeconds(3))) + .Build(); + + Connection.Closed += Connection_Closed; + Connection.Reconnected += Connection_Reconnected; + } + + private Task Connection_Closed(Exception? arg) + { + ConnectionStateChanged?.Invoke(Connection.State); + return Task.CompletedTask; + } + private Task Connection_Reconnected(string? arg) + { + ConnectionStateChanged?.Invoke(Connection.State); + return Task.CompletedTask; + } + + public virtual async Task StartAsync() + { + if (Connection.State == HubConnectionState.Disconnected) + { + await Connection.StartAsync(); + ConnectionStateChanged?.Invoke(Connection.State); + } + } + + public virtual async Task StopAsync() + { + if (Connection.State != HubConnectionState.Disconnected) + { + await Connection.StopAsync(); + ConnectionStateChanged?.Invoke(Connection.State); + } + } + + public class HubClientRepeatRetryPolicy(TimeSpan repeatSpan) : IRetryPolicy + { + private readonly TimeSpan RepeatTimeSpan = repeatSpan; + + public TimeSpan? NextRetryDelay(RetryContext retryContext) => RepeatTimeSpan; + } +} diff --git a/RobotNet.Clients/RobotNet.Clients.csproj b/RobotNet.Clients/RobotNet.Clients.csproj new file mode 100644 index 0000000..af7dfa4 --- /dev/null +++ b/RobotNet.Clients/RobotNet.Clients.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/RobotNet.IdentityServer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs b/RobotNet.IdentityServer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs new file mode 100644 index 0000000..2e359c1 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using RobotNet.IdentityServer.Data; +using System.Security.Claims; + +namespace Microsoft.AspNetCore.Routing +{ + internal static class IdentityComponentsEndpointRouteBuilderExtensions + { + // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. + public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var accountGroup = endpoints.MapGroup("/Account"); + + accountGroup.MapPost("/Logout", async ( + ClaimsPrincipal user, + SignInManager signInManager, + [FromForm] string returnUrl) => + { + await signInManager.SignOutAsync(); + return TypedResults.LocalRedirect($"~/{returnUrl}"); + }); + + return accountGroup; + } + } +} diff --git a/RobotNet.IdentityServer/Components/Account/IdentityNoOpEmailSender.cs b/RobotNet.IdentityServer/Components/Account/IdentityNoOpEmailSender.cs new file mode 100644 index 0000000..7f7c10a --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/IdentityNoOpEmailSender.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using RobotNet.IdentityServer.Data; + +namespace RobotNet.IdentityServer.Components.Account +{ + // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. + internal sealed class IdentityNoOpEmailSender : IEmailSender + { + private readonly IEmailSender emailSender = new NoOpEmailSender(); + + public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => + emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); + + public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); + + public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => + emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); + } +} diff --git a/RobotNet.IdentityServer/Components/Account/IdentityRedirectManager.cs b/RobotNet.IdentityServer/Components/Account/IdentityRedirectManager.cs new file mode 100644 index 0000000..7c264e5 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/IdentityRedirectManager.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Components; +using System.Diagnostics.CodeAnalysis; + +namespace RobotNet.IdentityServer.Components.Account +{ + internal sealed class IdentityRedirectManager(NavigationManager navigationManager) + { + public const string StatusCookieName = "Identity.StatusMessage"; + + private static readonly CookieBuilder StatusCookieBuilder = new() + { + SameSite = SameSiteMode.Strict, + HttpOnly = true, + IsEssential = true, + MaxAge = TimeSpan.FromSeconds(5), + }; + + [DoesNotReturn] + public void RedirectTo(string? uri) + { + uri ??= ""; + + // Prevent open redirects. + if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) + { + uri = navigationManager.ToBaseRelativePath(uri); + } + + // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. + // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. + navigationManager.NavigateTo(uri); + throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); + } + + [DoesNotReturn] + public void RedirectTo(string uri, Dictionary queryParameters) + { + var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); + var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); + RedirectTo(newUri); + } + + [DoesNotReturn] + public void RedirectToWithStatus(string uri, string message, HttpContext context) + { + context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); + RedirectTo(uri); + } + + private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); + + [DoesNotReturn] + public void RedirectToCurrentPage() => RedirectTo(CurrentPath); + + [DoesNotReturn] + public void RedirectToCurrentPageWithStatus(string message, HttpContext context) + => RedirectToWithStatus(CurrentPath, message, context); + } +} diff --git a/RobotNet.IdentityServer/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs b/RobotNet.IdentityServer/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000..a01075b --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/IdentityRevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using RobotNet.IdentityServer.Data; +using System.Security.Claims; + +namespace RobotNet.IdentityServer.Components.Account +{ + // This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user + // every 30 minutes an interactive circuit is connected. + internal sealed class IdentityRevalidatingAuthenticationStateProvider( + ILoggerFactory loggerFactory, + IServiceScopeFactory scopeFactory, + IOptions options) + : RevalidatingServerAuthenticationStateProvider(loggerFactory) + { + protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); + + protected override async Task ValidateAuthenticationStateAsync( + AuthenticationState authenticationState, CancellationToken cancellationToken) + { + // Get the user manager from a new scope to ensure it fetches fresh data + await using var scope = scopeFactory.CreateAsyncScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await ValidateSecurityStampAsync(userManager, authenticationState.User); + } + + private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) + { + var user = await userManager.GetUserAsync(principal); + if (user is null) + { + return false; + } + else if (!userManager.SupportsUserSecurityStamp) + { + return true; + } + else + { + var principalStamp = principal.FindFirstValue(options.Value.ClaimsIdentity.SecurityStampClaimType); + var userStamp = await userManager.GetSecurityStampAsync(user); + return principalStamp == userStamp; + } + } + } +} diff --git a/RobotNet.IdentityServer/Components/Account/IdentityUserAccessor.cs b/RobotNet.IdentityServer/Components/Account/IdentityUserAccessor.cs new file mode 100644 index 0000000..b50edb1 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/IdentityUserAccessor.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using RobotNet.IdentityServer.Data; + +namespace RobotNet.IdentityServer.Components.Account +{ + internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager) + { + public async Task GetRequiredUserAsync(HttpContext context) + { + var user = await userManager.GetUserAsync(context.User); + + if (user is null) + { + redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); + } + + return user; + } + } +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/AccessLogin.razor b/RobotNet.IdentityServer/Components/Account/Pages/AccessLogin.razor new file mode 100644 index 0000000..c901146 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/AccessLogin.razor @@ -0,0 +1,35 @@ +@page "/Account/Login/Access" +@using Microsoft.Extensions.Primitives +@using Microsoft.AspNetCore.Antiforgery; + +@attribute [RequireAntiforgeryToken] + +
+
+

Authorization

+ +

Do you want to grant @ApplicationName access to your data? (scopes requested: @Scope)

+ +
+ + @foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable>)HttpContext.Request.Form : HttpContext.Request.Query) + { + + } + + + + +
+
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromQuery(Name = "request_app")] + private string ApplicationName { get; set; } = ""; + + [SupplyParameterFromQuery(Name = "request_scope")] + private string Scope { get; set; } = ""; +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor b/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor new file mode 100644 index 0000000..ba93890 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor @@ -0,0 +1,355 @@ +@rendermode InteractiveServer + +@using Microsoft.AspNetCore.Identity +@using RobotNet.IdentityServer.Data +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components +@using Microsoft.EntityFrameworkCore +@using System.Threading +@using RobotNet.IdentityServer.Services +@using System.Text.RegularExpressions +@using System.ComponentModel.DataAnnotations + +@inherits LayoutComponentBase + +@inject RobotNet.IdentityServer.Services.UserImageService UserImageService +@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject UserManager UserManager +@inject RoleManager RoleManager +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + + + + + + +
+ + @if (userInfo != null) + { + + + + Thông tin cá nhân + Quản lý thông tin hồ sơ của bạn + + + @string.Join(", ", userRoles) + + + + + + +
+ + + +
+ @userInfo.FullName + + ID: @(userInfo.Id.Length > 10 ? userInfo.Id.Substring(0, 10) + "..." : userInfo.Id) + +
+ + + + + + + + + + + + + + + @if (!isButtonDisabled) + { + + + Hủy + + + Lưu thay đổi + + + } + + +
+
+ + +
+ } + else + { + + + + Vui lòng đăng nhập + + Bạn cần đăng nhập để xem và chỉnh sửa thông tin cá nhân. + + + Đăng nhập ngay + + + + } +
+
+ + + +
+
Thay đổi ảnh hồ sơ
+ + Ảnh hồ sơ giúp người khác nhận ra bạn và xác nhận rằng bạn đã đăng nhập. + + +
+ +
+ + + + Thay đổi + + +
+
+ + + + Xác nhận + + + Hủy + + +
+ +@code { + MudForm? form; + private string? avatarPreview; + private string? avatarUrl; + private IBrowserFile? selectedFile; + private bool ChangeAvatarVisible = false; + + private bool isButtonDisabled = true; + private string originalFullName = ""; + private string originalEmail = ""; + private string originalPhoneNumber = ""; + private string originalUserName = ""; + + private ApplicationUser? userInfo; + private List userRoles = new List(); + + private void EnableButtons() + { + isButtonDisabled = false; + } + + private void ChangeAvatar() + { + ChangeAvatarVisible = true; + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authenticationState.User; + + if (user?.Identity?.IsAuthenticated == true) + { + userInfo = await UserManager.GetUserAsync(user); + if (userInfo != null) + { + userRoles = (await UserManager.GetRolesAsync(userInfo)).ToList(); + + originalUserName = userInfo.UserName?? string.Empty; + originalFullName = userInfo.FullName?? string.Empty; + originalEmail = userInfo.Email?? string.Empty; + originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty; + + if (userInfo.AvatarImage != null) + { + avatarUrl = $"data:{userInfo.AvatarContentType};base64,{Convert.ToBase64String(userInfo.AvatarImage)}"; + avatarPreview = avatarUrl; + } + else + { + avatarUrl = "/uploads/avatars/anh.jpg"; + avatarPreview = avatarUrl; + } + } + } + else + { + Snackbar.Add("Vui lòng đăng nhập để tiếp tục", Severity.Error); + } + } + + private async Task HandleSelected(InputFileChangeEventArgs e) + { + selectedFile = e.File; + const long maxSize = 5 * 1024 * 1024; + + if (selectedFile.Size > maxSize) + { + Snackbar.Add("⚠️ Ảnh bạn chọn vượt quá 5MB. Vui lòng chọn ảnh nhỏ hơn.", Severity.Warning); + avatarPreview = avatarUrl; + selectedFile = null; + return; + } + try + { + (byte[] buffer, string contentType) = await UserImageService.ResizeAndConvertAsync(selectedFile.OpenReadStream()); + avatarPreview = $"data:{contentType};base64,{Convert.ToBase64String(buffer)}"; + if (userInfo != null) + { + userInfo.AvatarImage = buffer; + userInfo.AvatarContentType = selectedFile.ContentType; + } + } + catch (Exception ex) + { + Snackbar.Add($"❌ Lỗi khi đọc ảnh: {ex.Message}", Severity.Error); + avatarPreview = avatarUrl; + } + } + private async Task ConfirmChangeAvatar() + { + if (userInfo != null && userInfo.AvatarImage != null) + { + var result = await UserManager.UpdateAsync(userInfo); + if (result.Succeeded) + { + avatarUrl = avatarPreview; + + ChangeAvatarVisible = false; + + await Task.Delay(200); + + await UserInfoService.NotifyUserInfoChanged(); + + StateHasChanged(); + NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); + Snackbar.Add("Cập nhật ảnh đại diện thành công!", Severity.Success); + } + else + { + Snackbar.Add("Lỗi khi cập nhật avatar.", Severity.Error); + } + } + } + private void ResetFields() + { + if (userInfo == null) return; + userInfo.FullName = originalFullName; + userInfo.Email = originalEmail; + userInfo.PhoneNumber = originalPhoneNumber; + userInfo.UserName = originalUserName; + + isButtonDisabled = true; + } + + private async Task SaveUserInfo() + { + if (userInfo != null) + { + try + { + var result = await UserManager.UpdateAsync(userInfo); + if (result.Succeeded) + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var userInfo = await UserManager.GetUserAsync(authState.User); + if(userInfo != null) + { + originalFullName = userInfo.FullName ?? string.Empty; + originalEmail = userInfo.Email ?? string.Empty; + originalPhoneNumber = userInfo.PhoneNumber ?? string.Empty; + originalUserName = userInfo.UserName ?? string.Empty; + } + isButtonDisabled = true; + + await Task.Delay(200); + + await UserInfoService.NotifyUserInfoChanged(); + StateHasChanged(); + Snackbar.Add("Thông tin đã được cập nhật!", Severity.Success); + + } + else + { + Snackbar.Add("Lỗi khi cập nhật thông tin.", Severity.Error); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error while saving user information: {ex.Message}", Severity.Error); + } + } + } + +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor.css new file mode 100644 index 0000000..0df81e8 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Infor.razor.css @@ -0,0 +1,7 @@ +.mdi { + display: inline-flex; + justify-content: center; + align-items: center; + background-size: cover; + margin-top:7px; +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Login.razor b/RobotNet.IdentityServer/Components/Account/Pages/Login.razor new file mode 100644 index 0000000..c12fc89 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Login.razor @@ -0,0 +1,118 @@ +@page "/Account/Login" + +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using RobotNet.IdentityServer.Data + +@inject SignInManager SignInManager +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + +Log in + +
+

Log in

+ @if (!string.IsNullOrEmpty(errorMessage)) + { + var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success"; + + } + + +
+ +
+ + + +
+
+ + + +
+
+ +
+
+ +
+
+
+ + +@code { + private string? errorMessage; + + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + protected override async Task OnInitializedAsync() + { + if (HttpMethods.IsGet(HttpContext.Request.Method)) + { + // Clear the existing external cookie to ensure a clean login process + await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); + } + + errorMessage = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; + + if (errorMessage is not null) + { + HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); + } + } + + public async Task LoginUser() + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await SignInManager.PasswordSignInAsync(Input.Username, Input.Password, Input.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + Logger.LogInformation("User logged in."); + RedirectManager.RedirectTo(ReturnUrl); + } + else if (result.RequiresTwoFactor) + { + RedirectManager.RedirectTo( + "Account/LoginWith2fa", + new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); + } + else if (result.IsLockedOut) + { + Logger.LogWarning("User account locked out."); + RedirectManager.RedirectTo("Account/Lockout"); + } + else + { + errorMessage = "Error: Invalid login attempt."; + } + } + + private sealed class InputModel + { + [Required] + public string Username { get; set; } = ""; + + [Required] + [DataType(DataType.Password)] + public string Password { get; set; } = ""; + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/LogoutConfirm.razor b/RobotNet.IdentityServer/Components/Account/Pages/LogoutConfirm.razor new file mode 100644 index 0000000..570f705 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/LogoutConfirm.razor @@ -0,0 +1,37 @@ +@page "/Account/Logout/Confirm" + +@using Microsoft.EntityFrameworkCore.Metadata.Internal +@using Microsoft.Extensions.Primitives +@using Microsoft.AspNetCore.Antiforgery; + +@attribute [RequireAntiforgeryToken] + +@inject NavigationManager Navigation + +
+
+

Log out

+

Are you sure you want to sign out?

+ +
+ + @foreach (var parameter in HttpContext.Request.HasFormContentType ? (IEnumerable>)HttpContext.Request.Form : HttpContext.Request.Query) + { + + } + + + +
+
+ +@code { + [CascadingParameter] + private HttpContext HttpContext { get; set; } = default!; + private Task OnSubmitLogout() + { + + Navigation.NavigateTo("/Account/Login", forceLoad: true); + return Task.CompletedTask; + } +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor new file mode 100644 index 0000000..bb44bd1 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor @@ -0,0 +1,982 @@ +@rendermode InteractiveServer +@attribute [Authorize] + +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Authorization +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Components +@using OpenIddict.Abstractions +@using RobotNet.IdentityServer.Data +@using Microsoft.EntityFrameworkCore +@using System.Threading +@using static OpenIddict.Abstractions.OpenIddictConstants + +@inherits LayoutComponentBase + +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager NavigationManager +@inject IOpenIddictApplicationManager ApplicationManager +@inject IOpenIddictScopeManager ScopeManager + + + + + + + + +
+ +
+ + OpenIddict Manager + + + Quản lý ứng dụng OAuth2 & OpenID Connect một cách dễ dàng + +
+ + @filteredApplications.Count Apps + + +
+
+ + + +
+
+ + Danh sách Application + + + Thêm Application + +
+
+ + + + + Client + Type + Display Name + Secret + Endpoints + Actions + + + + + @context.ClientId + @context.Id[..8]... + + + + + @(context.ClientType == ClientTypes.Confidential ? "Confidential" : "Public") + + + + @context.DisplayName + + +
+ @if (!string.IsNullOrEmpty(context.ClientSecret)) + { + + Yes + } + else + { + + No + } +
+
+ + @if (context.RedirectUris.Any()) + { + + + @context.RedirectUris.Count URIs + + + } + else + { + No URIs + } + + +
+ + + +
+
+
+ + + +
+
+
+ + + + + + + + @(editingApplication != null ? "Chỉnh sửa Application" : "Tạo Application Mới") + + + + + + + + + + Thông tin cơ bản + + + + + + + + + + + + + + Public + Confidential + + + + + + Explicit + Implicit + + + + @if (applicationForm.ClientType == ClientTypes.Confidential) + { + + + + } + + + + + + Cấu hình Endpoints + + + + +
+ Redirect URIs + + + @foreach (var uri in applicationForm.RedirectUris) + { + + } + +
+
+ + +
+ Post Logout URIs + + + @foreach (var uri in applicationForm.PostLogoutRedirectUris) + { + + } + +
+
+ + + + + + Permissions & Requirements + + + + + + @foreach (var permission in permissionChecks) + { + + @permission.Key.Split('.').Last() + + } + + + + @foreach (var selectedPermission in GetSelectedPermissions()) + { + + } + + + @if (applicationForm.ClientType == ClientTypes.Public) + { + + Requirements + @foreach (var requirement in requirementChecks) + { + + } + + } +
+
+
+ + + Hủy + + + @(editingApplication != null ? "Cập nhật" : "Tạo mới") + + +
+ + + + + + + Chi tiết Application + + + + @if (selectedApplication != null) + { + + + + + Thông tin cơ bản + +
ID: @selectedApplication.Id
+
Client ID: @selectedApplication.ClientId
+
Display Name: @selectedApplication.DisplayName
+
Type: @selectedApplication.ClientType
+
Consent: @selectedApplication.ConsentType
+
+
+
+ + + + 🔒 Bảo mật +
+ Client Secret: + @if (!string.IsNullOrEmpty(selectedApplication.ClientSecret)) + { + + } + else + { + Không + } +
+
+
+ + @if (selectedApplication.RedirectUris.Any()) + { + + + 🔗 Redirect URIs + + @foreach (var uri in selectedApplication.RedirectUris) + { + + } + + + + } + + @if (selectedApplication.Permissions.Any()) + { + + + 🛡️ Permissions + + @foreach (var permission in selectedApplication.Permissions) + { + + } + + + + } + @if (selectedApplication.Requirements.Any()) + { + + + ⚙️ Requirements + @if (selectedApplication.Requirements.Any()) + { + + @foreach (var requirement in selectedApplication.Requirements) + { + + } + + } + else + { + + Không có requirements nào được thiết lập + + } + + + } + +
+
+ } +
+ + + Đóng + + +
+ + +@code { + private List filteredApplications = new(); + private bool loadingApplications = false; + private bool ShowApplicationDialog = false; + private bool ShowDetailsDialog = false; + private bool showClientSecret = false; + private ApplicationInfo? editingApplication = null; + private ApplicationInfo? selectedApplication = null; + private ApplicationForm applicationForm = new(); + private string redirectUriInput = string.Empty; + private string postLogoutUriInput = string.Empty; + private string customScopeInput = string.Empty; + private HashSet customScopes = new(); + private Dictionary permissionChecks = new(); + private Dictionary requirementChecks = new(); + private List availableScopes = new(); + public class ApplicationInfo + { + public string Id { get; set; } = string.Empty; + public string ApplicationType { get; set; } = string.Empty; + public string ClientId { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string ClientType { get; set; } = string.Empty; + public string ConsentType { get; set; } = string.Empty; + public List RedirectUris { get; set; } = new(); + public List PostLogoutRedirectUris { get; set; } = new(); + public List Permissions { get; set; } = new(); + public List Requirements { get; set; } = new(); + public string? ClientSecret { get; set; } + } + + public class ApplicationForm + { + public string ClientId { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string ClientType { get; set; } = ClientTypes.Public; + public string ConsentType { get; set; } = ConsentTypes.Explicit; + public string? ClientSecret { get; set; } + public List RedirectUris { get; set; } = new(); + public List PostLogoutRedirectUris { get; set; } = new(); + } + + private string applicationSearchTerm = ""; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity?.IsAuthenticated == true) + { + InitializePermissionChecks(); + InitializeRequirementChecks(); + await LoadApplicationsAsync(); + await LoadAvailableScopesAsync(); + } + } + + private void InitializePermissionChecks() + { + permissionChecks.Clear(); + + permissionChecks.Add(Permissions.Endpoints.Authorization, false); + permissionChecks.Add(Permissions.Endpoints.EndSession, false); + permissionChecks.Add(Permissions.Endpoints.Token, false); + permissionChecks.Add(Permissions.Endpoints.Introspection, false); + permissionChecks.Add(Permissions.GrantTypes.AuthorizationCode, false); + permissionChecks.Add(Permissions.GrantTypes.RefreshToken, false); + permissionChecks.Add(Permissions.GrantTypes.ClientCredentials, false); + permissionChecks.Add(Permissions.ResponseTypes.Code, false); + permissionChecks.Add(Permissions.ResponseTypes.Token, false); + permissionChecks.Add(Permissions.Scopes.Email, false); + permissionChecks.Add(Permissions.Scopes.Profile, false); + permissionChecks.Add(Permissions.Scopes.Roles, false); + + foreach (var scope in availableScopes) + { + var scopePermission = Permissions.Prefixes.Scope + scope; + if (!permissionChecks.ContainsKey(scopePermission)) + { + permissionChecks.Add(scopePermission, false); + } + } + } + + private void InitializeRequirementChecks() + { + requirementChecks = new() { + { Requirements.Features.ProofKeyForCodeExchange, false } + }; + } + + private void OnClientTypeChanged(string newClientType) + { + applicationForm.ClientType = newClientType; + if (newClientType == ClientTypes.Public) + { + applicationForm.ClientSecret = null; + } + StateHasChanged(); + } + + private void OnPermissionSelectionChanged(IEnumerable selectedValues) + { + foreach (var key in permissionChecks.Keys.ToList()) + { + permissionChecks[key] = false; + } + + foreach (var value in selectedValues) + { + if (permissionChecks.ContainsKey(value)) + { + permissionChecks[value] = true; + } + } + + StateHasChanged(); + } + + private IEnumerable GetSelectedPermissions() + { + return permissionChecks.Where(x => x.Value).Select(x => x.Key); + } + + private void RemovePermission(string permission) + { + if (permissionChecks.ContainsKey(permission)) + { + permissionChecks[permission] = false; + StateHasChanged(); + } + } + + private IEnumerable FilteredApplications => + string.IsNullOrWhiteSpace(applicationSearchTerm) + ? filteredApplications + : filteredApplications.Where(r => + (r.ClientId != null && r.ClientId.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) || + (r.DisplayName != null && r.DisplayName.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) || + (r.ClientType != null && r.ClientType.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase)) || + (r.Id != null && r.Id.Contains(applicationSearchTerm, StringComparison.OrdinalIgnoreCase))); + + private async Task LoadApplicationsAsync() + { + try + { + loadingApplications = true; + filteredApplications.Clear(); + + await foreach (var app in ApplicationManager.ListAsync()) + { + string? clientSecret = null; + try + { + var properties = await ApplicationManager.GetPropertiesAsync(app); + clientSecret = properties.ContainsKey("client_secret") ? properties["client_secret"].ToString() : null; + if (string.IsNullOrEmpty(clientSecret)) + { + var clientType = await ApplicationManager.GetClientTypeAsync(app); + if (clientType == ClientTypes.Confidential) + { + clientSecret = "***"; + } + } + } + catch + { + var clientType = await ApplicationManager.GetClientTypeAsync(app); + if (clientType == ClientTypes.Confidential) + { + clientSecret = "***"; + } + } + + filteredApplications.Add(new ApplicationInfo + { + Id = await ApplicationManager.GetIdAsync(app) ?? string.Empty, + ApplicationType = await ApplicationManager.GetApplicationTypeAsync(app) ?? string.Empty, + ClientId = await ApplicationManager.GetClientIdAsync(app) ?? string.Empty, + DisplayName = await ApplicationManager.GetDisplayNameAsync(app) ?? string.Empty, + ClientType = await ApplicationManager.GetClientTypeAsync(app) ?? string.Empty, + ConsentType = await ApplicationManager.GetConsentTypeAsync(app) ?? string.Empty, + RedirectUris = (await ApplicationManager.GetRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(), + PostLogoutRedirectUris = (await ApplicationManager.GetPostLogoutRedirectUrisAsync(app)).Select(u => u.ToString()).ToList(), + Permissions = (await ApplicationManager.GetPermissionsAsync(app)).ToList(), + Requirements = (await ApplicationManager.GetRequirementsAsync(app)).ToList(), + ClientSecret = clientSecret + }); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải applications: {ex.Message}", Severity.Error); + } + finally + { + loadingApplications = false; + StateHasChanged(); + } + } + + + private async Task LoadAvailableScopesAsync() + { + try + { + availableScopes.Clear(); + + await foreach (var scope in ScopeManager.ListAsync()) + { + var scopeName = await ScopeManager.GetNameAsync(scope); + if (!string.IsNullOrEmpty(scopeName)) + { + availableScopes.Add(scopeName); + } + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error); + } + } + private void ViewApplicationDetails(ApplicationInfo application) + { + selectedApplication = application; + showClientSecret = false; + ShowDetailsDialog = true; + } + + private void CloseDetailsDialog() + { + ShowDetailsDialog = false; + selectedApplication = null; + showClientSecret = false; + } + + private void ToggleClientSecretVisibility() + { + showClientSecret = !showClientSecret; + } + + private async void OpenApplicationDialog(ApplicationInfo? application = null) + { + await LoadAvailableScopesAsync(); + InitializePermissionChecks(); + ShowApplicationDialog = true; + editingApplication = application; + ResetApplicationForm(); + + if (application != null) + { + applicationForm = new ApplicationForm + { + ClientId = application.ClientId, + DisplayName = application.DisplayName, + ClientType = application.ClientType, + ConsentType = application.ConsentType, + RedirectUris = new(application.RedirectUris), + PostLogoutRedirectUris = new(application.PostLogoutRedirectUris), + + ClientSecret = application.ClientType == ClientTypes.Confidential ? string.Empty : null + }; + + foreach (var permission in application.Permissions) + { + if (permissionChecks.ContainsKey(permission)) permissionChecks[permission] = true; + else if (permission.StartsWith(Permissions.Prefixes.Scope)) customScopes.Add(permission[Permissions.Prefixes.Scope.Length..]); + } + + foreach (var requirement in application.Requirements) + { + if (requirementChecks.ContainsKey(requirement)) requirementChecks[requirement] = true; + } + } + StateHasChanged(); + } + + private void EditApplication(ApplicationInfo application) => OpenApplicationDialog(application); + + private void ResetApplicationForm() + { + applicationForm = new(); + redirectUriInput = string.Empty; + postLogoutUriInput = string.Empty; + customScopeInput = string.Empty; + customScopes.Clear(); + + foreach (var key in permissionChecks.Keys.ToList()) permissionChecks[key] = false; + foreach (var key in requirementChecks.Keys.ToList()) requirementChecks[key] = false; + } + + private void AddRedirectUri() + { + if (!string.IsNullOrWhiteSpace(redirectUriInput)) + { + if (Uri.TryCreate(redirectUriInput.Trim(), UriKind.Absolute, out _)) + { + if (!applicationForm.RedirectUris.Contains(redirectUriInput.Trim())) + { + applicationForm.RedirectUris.Add(redirectUriInput.Trim()); + redirectUriInput = string.Empty; + StateHasChanged(); + } + else + { + Snackbar.Add("URI này đã tồn tại", Severity.Warning); + } + } + else + { + Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/login-callback)", Severity.Error); + } + } + } + + private void RemoveRedirectUri(string uri) => applicationForm.RedirectUris.Remove(uri); + + private void AddPostLogoutUri() + { + if (!string.IsNullOrWhiteSpace(postLogoutUriInput)) + { + + if (Uri.TryCreate(postLogoutUriInput.Trim(), UriKind.Absolute, out _)) + { + if (!applicationForm.PostLogoutRedirectUris.Contains(postLogoutUriInput.Trim())) + { + applicationForm.PostLogoutRedirectUris.Add(postLogoutUriInput.Trim()); + postLogoutUriInput = string.Empty; + StateHasChanged(); + } + else + { + Snackbar.Add("URI này đã tồn tại", Severity.Warning); + } + } + else + { + Snackbar.Add("URI không hợp lệ. Vui lòng nhập URI đầy đủ (ví dụ: https://example.com/logout-callback)", Severity.Error); + } + } + } + + + private void RemovePostLogoutUri(string uri) => applicationForm.PostLogoutRedirectUris.Remove(uri); + + private void AddCustomScope() + { + if (!string.IsNullOrWhiteSpace(customScopeInput) && !customScopes.Contains(customScopeInput)) + { + customScopes.Add(customScopeInput); + customScopeInput = string.Empty; + } + } + + private void RemoveCustomScope(string scope) => customScopes.Remove(scope); + + private async Task SaveApplication() + { + try + { + if (string.IsNullOrWhiteSpace(applicationForm.ClientId)) + { + Snackbar.Add("Client ID là bắt buộc", Severity.Error); + return; + } + + if (applicationForm.ClientType == ClientTypes.Confidential) + { + if (editingApplication == null && string.IsNullOrWhiteSpace(applicationForm.ClientSecret)) + { + Snackbar.Add("Client Secret là bắt buộc cho Confidential client", Severity.Error); + return; + } + } + + if (editingApplication != null) + { + var existingApp = await ApplicationManager.FindByClientIdAsync(editingApplication.ClientId); + if (existingApp != null) + { + var descriptor = new OpenIddictApplicationDescriptor + { + ClientId = applicationForm.ClientId, + DisplayName = applicationForm.DisplayName, + ClientType = applicationForm.ClientType, + ConsentType = applicationForm.ConsentType + }; + + if (applicationForm.ClientType == ClientTypes.Confidential) + { + if (!string.IsNullOrWhiteSpace(applicationForm.ClientSecret)) + { + descriptor.ClientSecret = applicationForm.ClientSecret; + } + } + else if (applicationForm.ClientType == ClientTypes.Public) + { + descriptor.ClientSecret = null; + } + + var currentDescriptor = new OpenIddictApplicationDescriptor(); + await ApplicationManager.PopulateAsync(currentDescriptor, existingApp); + + + if (applicationForm.ClientType == ClientTypes.Confidential && + string.IsNullOrWhiteSpace(applicationForm.ClientSecret)) + { + descriptor.ClientSecret = currentDescriptor.ClientSecret; + } + + + foreach (var uriString in applicationForm.RedirectUris) + { + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + descriptor.RedirectUris.Add(uri); + } + else + { + Snackbar.Add($"Redirect URI không hợp lệ: {uriString}", Severity.Error); + return; + } + } + + + foreach (var uriString in applicationForm.PostLogoutRedirectUris) + { + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + descriptor.PostLogoutRedirectUris.Add(uri); + } + else + { + Snackbar.Add($"Post Logout URI không hợp lệ: {uriString}", Severity.Error); + return; + } + } + + + permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key)); + customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope)); + + requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key)); + + await ApplicationManager.UpdateAsync(existingApp, descriptor); + } + } + else + { + var descriptor = new OpenIddictApplicationDescriptor + { + ClientId = applicationForm.ClientId, + DisplayName = applicationForm.DisplayName, + ClientType = applicationForm.ClientType, + ConsentType = applicationForm.ConsentType, + ClientSecret = applicationForm.ClientType == ClientTypes.Confidential ? applicationForm.ClientSecret : null + }; + + foreach (var uriString in applicationForm.RedirectUris) + { + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + descriptor.RedirectUris.Add(uri); + } + } + + foreach (var uriString in applicationForm.PostLogoutRedirectUris) + { + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + { + descriptor.PostLogoutRedirectUris.Add(uri); + } + } + + permissionChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Permissions.Add(kvp.Key)); + customScopes.ToList().ForEach(scope => descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope)); + requirementChecks.Where(x => x.Value).ToList().ForEach(kvp => descriptor.Requirements.Add(kvp.Key)); + + await ApplicationManager.CreateAsync(descriptor); + } + + Snackbar.Add(editingApplication != null ? "Cập nhật application thành công" : "Tạo application thành công", Severity.Success); + ShowApplicationDialog = false; + await LoadApplicationsAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi lưu application: {ex.Message}", Severity.Error); + } + } + + + private void CancelApplicationDialog() + { + ShowApplicationDialog = false; + ResetApplicationForm(); + } + + private async Task DeleteApplication(string clientId) + { + var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa application '{clientId}'?", yesText: "Xóa", cancelText: "Hủy"); + if (confirm == true) + { + try + { + var app = await ApplicationManager.FindByClientIdAsync(clientId); + if (app != null) + { + await ApplicationManager.DeleteAsync(app); + Snackbar.Add("Xóa application thành công", Severity.Success); + await LoadApplicationsAsync(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi xóa application: {ex.Message}", Severity.Error); + } + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor.css new file mode 100644 index 0000000..1782e08 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictApplication.razor.css @@ -0,0 +1,66 @@ +.mdi { + display: inline-block; + position: relative; + background-size: cover; + align-items: center; +} +.app-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 16px; + padding: 2rem; + color: white; + margin-bottom: 2rem; + box-shadow: 0 20px 40px rgba(102, 126, 234, 0.3); +} + +.glass-card { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.compact-table { + font-size: 0.875rem; +} + + .compact-table .mud-table-cell { + padding: 8px 12px; + } + +.action-buttons { + display: flex; + gap: 4px; +} + +.status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 0.75rem; + font-weight: 600; +} + +.floating-add-btn { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 1000; + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4); +} + +.permission-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + max-height: 300px; + overflow-y: auto; +} + +.uri-input-section { + background: rgba(102, 126, 234, 0.05); + border-radius: 12px; + padding: 16px; + margin: 12px 0; +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictManager.razor b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictManager.razor new file mode 100644 index 0000000..a5b0f44 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictManager.razor @@ -0,0 +1,21 @@ +@page "/Account/OpenIdDictManager" + +@rendermode InteractiveServer +@using MudBlazor + + + + + + + + + + + + + + +@code { + +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor new file mode 100644 index 0000000..2a05df7 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor @@ -0,0 +1,734 @@ +@rendermode InteractiveServer +@attribute [Authorize] + +@using Microsoft.AspNetCore.Authorization +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Components +@using OpenIddict.Abstractions +@using RobotNet.IdentityServer.Data +@using Microsoft.EntityFrameworkCore +@using System.Threading +@using static OpenIddict.Abstractions.OpenIddictConstants + +@inherits LayoutComponentBase + +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager NavigationManager +@inject IOpenIddictApplicationManager ApplicationManager +@inject IOpenIddictScopeManager ScopeManager + + + + + +
+
+
+ +
+ + + OpenIddict Scopes + +
+
+ + Quản lý phạm vi truy cập OAuth2 & OpenID Connect + +
+
+
+
+ + + @filteredScopes.Count Scopes + +
+
+
+ +
+
+
+ + Danh sách Scopes + +
+ + Làm mới + + + Thêm Scope + +
+
+
+ + + + Tên hiển thị + Tên Scope + Resources + Thao tác + + + + + + @(string.IsNullOrEmpty(context.DisplayName) ? context.Name : context.DisplayName) + + @context.Id[..12]... + + + + + + + @{ + var validResources = GetValidResources(context.Resources); + } + @if (validResources.Any()) + { + GetResourceDisplayName(r)))"> + 1 ? "s" : "")}")" + Size="Size.Small" + Class="resource-chip" /> + + @if (context.Resources.Count > validResources.Count) + { + + + + } + } + else if (context.Resources.Any()) + { + + + + } + else + { + + Không có resources + + } + + +
+ + + @if (HasInvalidResources(context.Resources)) + { + + } + +
+
+
+ + + + +
+ + + Đang tải dữ liệu... + +
+
+
+
+ + + +
+ + + @(editingScope?.Name != null ? "Chỉnh sửa Scope" : "Thêm Scope mới") + +
+
+ +
+ + + + + + + + +
+ + + Chọn Resources + + + @foreach (var resource in availableResources) + { + +
+ +
+ @resource.DisplayName + @resource.ClientId +
+
+
+ } +
+ + @if (GetSelectedResources().Any()) + { + + Resources đã chọn: + +
+ @foreach (var selectedResource in GetSelectedResources()) + { + var resourceInfo = availableResources.FirstOrDefault(r => r.ClientId == selectedResource); + + } +
+ } +
+
+
+
+
+ + + Hủy + + + + Lưu + + +
+ + + +
+ + Chi tiết Scope +
+
+ + @if (selectedScope != null) + { +
+ + +
+ ID + @selectedScope.Id +
+
+ +
+ Tên + @selectedScope.Name +
+
+ +
+ Tên hiển thị + @(string.IsNullOrEmpty(selectedScope.DisplayName) ? "Không có" : selectedScope.DisplayName) +
+
+ +
+ Resources +
+ @if (selectedScope.Resources.Any()) + { + var validResources = GetValidResources(selectedScope.Resources); + var invalidResources = selectedScope.Resources.Except(validResources).ToList(); + + @if (validResources.Any()) + { + Resources hợp lệ: +
+ @foreach (var resource in validResources) + { + + } +
+ } + + @if (invalidResources.Any()) + { + Resources không hợp lệ: +
+ @foreach (var resource in invalidResources) + { + + } +
+ } + } + else + { + Không có resources + } +
+
+
+
+
+ } +
+ + + + Đóng + + +
+
+ + +@code { + private string resourceInput = string.Empty; + private List filteredScopes = new(); + private List availableResources = new(); + private Dictionary resourceChecks = new(); + private bool loadingScopes = false; + private bool showScopeDialog = false; + private bool ShowDetailsDialog = false; + private ScopeInfo? editingScope = null; + private ScopeInfo? selectedScope = null; + private ScopeForm scopeForm = new(); + + + + public class ScopeInfo + { + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Resources { get; set; } = new(); + public Dictionary Properties { get; set; } = new(); + } + + public class ScopeForm + { + public string Name { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List Resources { get; set; } = new(); + } + + public class ResourceInfo + { + public string ClientId { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity?.IsAuthenticated == true) + { + await LoadAvailableResourcesAsync(); + await LoadScopesAsync(); + } + } + + private async Task LoadAvailableResourcesAsync() + { + try + { + availableResources.Clear(); + + await foreach (var app in ApplicationManager.ListAsync()) + { + var clientType = await ApplicationManager.GetClientTypeAsync(app); + if (clientType == ClientTypes.Confidential) + { + var clientId = await ApplicationManager.GetClientIdAsync(app); + var displayName = await ApplicationManager.GetDisplayNameAsync(app); + + if (!string.IsNullOrEmpty(clientId)) + { + availableResources.Add(new ResourceInfo + { + ClientId = clientId, + DisplayName = string.IsNullOrEmpty(displayName) ? clientId : displayName + }); + } + } + } + + resourceChecks.Clear(); + foreach (var resource in availableResources) + { + resourceChecks[resource.ClientId] = false; + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải resources: {ex.Message}", Severity.Error); + } + } + + + private List GetValidResources(List resources) + { + var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet(); + return resources.Where(r => validResourceIds.Contains(r)).ToList(); + } + + + private bool HasInvalidResources(List resources) + { + var validResourceIds = availableResources.Select(r => r.ClientId).ToHashSet(); + return resources.Any(r => !validResourceIds.Contains(r)); + } + + + private string GetResourceDisplayName(string clientId) + { + var resource = availableResources.FirstOrDefault(r => r.ClientId == clientId); + return resource?.DisplayName ?? clientId; + } + + + private async Task CleanupScopeResources(ScopeInfo scope) + { + var confirm = await DialogService.ShowMessageBox( + "Xác nhận dọn dẹp", + $"Bạn có muốn xóa các resources không hợp lệ khỏi scope '{scope.Name}'?", + yesText: "Dọn dẹp", + cancelText: "Hủy" + ); + + if (confirm == true) + { + try + { + var existingScope = await ScopeManager.FindByNameAsync(scope.Name); + if (existingScope != null) + { + var validResources = GetValidResources(scope.Resources); + + var descriptor = new OpenIddictScopeDescriptor + { + Name = scope.Name, + DisplayName = scope.DisplayName, + Description = scope.Description + }; + + foreach (var resource in validResources) + { + descriptor.Resources.Add(resource); + } + + await ScopeManager.PopulateAsync(existingScope, descriptor); + await ScopeManager.UpdateAsync(existingScope); + + Snackbar.Add($"Đã dọn dẹp {scope.Resources.Count - validResources.Count} resources không hợp lệ", Severity.Success); + await LoadScopesAsync(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi dọn dẹp resources: {ex.Message}", Severity.Error); + } + } + } + + + private async Task RefreshScopesAsync() + { + await LoadAvailableResourcesAsync(); + await LoadScopesAsync(); + Snackbar.Add("Đã làm mới danh sách scopes", Severity.Success); + } + + private void AddResource() + { + if (!string.IsNullOrWhiteSpace(resourceInput) && !scopeForm.Resources.Contains(resourceInput)) + { + scopeForm.Resources.Add(resourceInput); + resourceInput = string.Empty; + StateHasChanged(); + } + } + + private void OnResourceSelectionChanged(IEnumerable selectedValues) + { + foreach (var key in resourceChecks.Keys.ToList()) + { + resourceChecks[key] = false; + } + + foreach (var value in selectedValues) + { + if (resourceChecks.ContainsKey(value)) + { + resourceChecks[value] = true; + } + } + + scopeForm.Resources = selectedValues.ToList(); + StateHasChanged(); + } + + private IEnumerable GetSelectedResources() + { + return resourceChecks.Where(x => x.Value).Select(x => x.Key); + } + + private void RemoveResource(string resource) + { + if (resourceChecks.ContainsKey(resource)) + { + resourceChecks[resource] = false; + scopeForm.Resources.Remove(resource); + StateHasChanged(); + } + } + + private async Task LoadScopesAsync() + { + try + { + loadingScopes = true; + filteredScopes.Clear(); + + await foreach (var scope in ScopeManager.ListAsync()) + { + var id = await ScopeManager.GetIdAsync(scope); + var name = await ScopeManager.GetNameAsync(scope); + var displayName = await ScopeManager.GetDisplayNameAsync(scope); + var description = await ScopeManager.GetDescriptionAsync(scope); + var resources = await ScopeManager.GetResourcesAsync(scope); + var properties = await ScopeManager.GetPropertiesAsync(scope); + + filteredScopes.Add(new ScopeInfo + { + Id = id ?? string.Empty, + Name = name ?? string.Empty, + DisplayName = displayName ?? string.Empty, + Description = description ?? string.Empty, + Resources = resources.ToList(), + Properties = properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToString() ?? string.Empty) + }); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải scopes: {ex.Message}", Severity.Error); + } + finally + { + loadingScopes = false; + StateHasChanged(); + } + } + + private void ViewScopeDetails(ScopeInfo scope) + { + selectedScope = scope; + ShowDetailsDialog = true; + } + + private void CloseDetailsDialog() + { + ShowDetailsDialog = false; + selectedScope = null; + } + + private async Task OpenScopeDialog(ScopeInfo? scope = null) + { + editingScope = scope; + ResetScopeForm(); + + if (scope != null) + { + scopeForm.Name = scope.Name; + scopeForm.DisplayName = scope.DisplayName; + scopeForm.Description = scope.Description; + + + var validResources = GetValidResources(scope.Resources); + scopeForm.Resources = new List(validResources); + + foreach (var key in resourceChecks.Keys.ToList()) + { + resourceChecks[key] = validResources.Contains(key); + } + + + if (scope.Resources.Count > validResources.Count) + { + var invalidCount = scope.Resources.Count - validResources.Count; + Snackbar.Add($"Đã loại bỏ {invalidCount} resource không hợp lệ khỏi form chỉnh sửa", Severity.Warning); + } + } + + showScopeDialog = true; + await Task.CompletedTask; + } + + private async void EditScope(ScopeInfo scope) + { + await OpenScopeDialog(scope); + } + + private void ResetScopeForm() + { + scopeForm = new ScopeForm(); + + foreach (var key in resourceChecks.Keys.ToList()) + { + resourceChecks[key] = false; + } + } + + private async Task SaveScope() + { + try + { + if (string.IsNullOrWhiteSpace(scopeForm.Name)) + { + Snackbar.Add("Name là bắt buộc", Severity.Error); + return; + } + + var descriptor = new OpenIddictScopeDescriptor + { + Name = scopeForm.Name, + DisplayName = scopeForm.DisplayName, + Description = scopeForm.Description + }; + + + var validResources = GetValidResources(scopeForm.Resources); + foreach (var resource in validResources) + { + descriptor.Resources.Add(resource); + } + + if (editingScope != null) + { + var existingScope = await ScopeManager.FindByNameAsync(editingScope.Name); + if (existingScope != null) + { + await ScopeManager.PopulateAsync(existingScope, descriptor); + await ScopeManager.UpdateAsync(existingScope); + } + } + else + { + await ScopeManager.CreateAsync(descriptor); + } + + Snackbar.Add(editingScope != null ? "Cập nhật scope thành công" : "Tạo scope thành công", Severity.Success); + showScopeDialog = false; + await LoadScopesAsync(); + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi lưu scope: {ex.Message}", Severity.Error); + } + } + + private void CancelScopeDialog() + { + showScopeDialog = false; + ResetScopeForm(); + } + + private async Task DeleteScope(string name) + { + var confirm = await DialogService.ShowMessageBox("Xác nhận xóa", $"Bạn có chắc chắn muốn xóa scope '{name}'?", yesText: "Xóa", cancelText: "Hủy"); + if (confirm == true) + { + try + { + var scope = await ScopeManager.FindByNameAsync(name); + if (scope != null) + { + await ScopeManager.DeleteAsync(scope); + Snackbar.Add("Xóa scope thành công", Severity.Success); + await LoadScopesAsync(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi xóa scope: {ex.Message}", Severity.Error); + } + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor.css new file mode 100644 index 0000000..18de40a --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/OpenIdDictScope.razor.css @@ -0,0 +1,142 @@ +.mdi { + display: inline-block; + position: relative; + background-size: cover; + align-items: center; +} +.textid { + font-family: 'Gill Sans', 'Gill Sans MT', 'Calibri', 'Trebuchet MS', 'sans-serif'; + color: #6b7280; +} +.header-gradient { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 16px; + color: white; + padding: 2rem; + margin-bottom: 1.5rem; + position: relative; + overflow: hidden; +} + + .header-gradient::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + opacity: 0.3; + } + +.header-content { + position: relative; + z-index: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.scope-icon { + font-size: 4rem; + margin-right: 1rem; + padding-left: 1rem; +} + +.stats-badge { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border-radius: 12px; + padding: 0.5rem 1rem; + border: 1px solid rgba(255, 255, 255, 0.3); + display: flex; + align-items: center; +} + +.scope-card { + background: white; + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.3s ease; + overflow: hidden; +} + + .scope-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); + } + +.scope-table { + background: transparent; +} + + .scope-table .mud-table-head { + background: #f8fafc; + border-bottom: 2px solid #e2e8f0; + } + + .scope-table .mud-table-head th { + font-weight: 600; + color: #334155; + padding: 1rem 0.75rem; + } + + .scope-table .mud-table-row { + border-bottom: 1px solid #f1f5f9; + transition: background-color 0.2s ease; + } + + .scope-table .mud-table-row:hover { + background-color: #f8fafc; + } + + .scope-table .mud-table-cell { + padding: 1rem 0.75rem; + vertical-align: middle; + } + +.action-buttons { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.add-scope-btn { + background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%); + border: none; + box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); + transition: all 0.3s ease; +} + + .add-scope-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4); + } + +.resource-chip { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + color: white; + border: none; + font-weight: 500; +} + +.dialog-content { + background: #fafbfc; + border-radius: 12px; + padding: 1.5rem; + margin: 1rem 0; +} + +.resource-selection { + background: white; + border-radius: 8px; + padding: 1rem; + border: 1px solid #e2e8f0; +} + +.selected-resource-chip { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + color: white; + margin: 0.25rem; +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Password.razor b/RobotNet.IdentityServer/Components/Account/Pages/Password.razor new file mode 100644 index 0000000..179cfb8 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Password.razor @@ -0,0 +1,285 @@ +@rendermode InteractiveServer + +@using Microsoft.AspNetCore.Identity +@using RobotNet.IdentityServer.Data +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components +@using System.Threading +@using System.ComponentModel.DataAnnotations +@using RobotNet.IdentityServer.Services + +@inherits LayoutComponentBase + +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject PasswordStrengthService PasswordStrengthService +@inject UserManager UserManager +@inject ISnackbar Snackbar +@inject NavigationManager NavigationManager + + + +
+
+ +
+
+ +
+ Đổi mật khẩu + Cập nhật mật khẩu để tăng cường bảo mật +
+
+
+ + + + + +
+
+ + +
+ +
+ + + + @if (!string.IsNullOrEmpty(model.NewPassword)) + { +
+ Độ mạnh: @GetPasswordStrengthText() + +
+ Yêu cầu: +
+
+ + Tối thiểu 6 ký tự +
+ @*
+ + Chữ hoa +
*@ +
+ + Chữ thường +
+ @*
+ + Số +
+
+ + Ký tự đặc biệt +
*@ +
+
+
+ } +
+ +
+ + + + @if (!string.IsNullOrEmpty(model.NewPassword) && !string.IsNullOrEmpty(model.ConfirmPassword)) + { + + @(model.NewPassword == model.ConfirmPassword ? "Mật khẩu khớp" : "Mật khẩu không khớp") + + } +
+
+ + @if (!string.IsNullOrEmpty(errorMessage)) + { + + @errorMessage + + } +
+
+ + + + Hủy + + + @if (isProcessing) + { + + Đang xử lý... + } + else + { + Lưu + } + + +
+
+
+ +@code { + private bool isButtonDisabled = true; + private bool showCurrentPassword = false; + private bool showNewPassword = false; + private bool showConfirmPassword = false; + private ChangePasswordModel model = new(); + private bool isProcessing = false; + private string errorMessage = string.Empty; + private EditForm? editForm; + + private class ChangePasswordModel + { + [Required(ErrorMessage = "Vui lòng nhập mật khẩu hiện tại")] + public string CurrentPassword { get; set; } = string.Empty; + + [Required(ErrorMessage = "Vui lòng nhập mật khẩu mới")] + [StringLength(100, ErrorMessage = "Mật khẩu phải từ {2} đến {1} ký tự", MinimumLength = 8)] + public string NewPassword { get; set; } = string.Empty; + + [Required(ErrorMessage = "Vui lòng xác nhận mật khẩu mới")] + [Compare("NewPassword", ErrorMessage = "Mật khẩu xác nhận không khớp")] + public string ConfirmPassword { get; set; } = string.Empty; + } + + private void EnableButtons() + { + isButtonDisabled = false; + } + + private void OnNewPasswordChanged(ChangeEventArgs e) + { + model.NewPassword = e.Value?.ToString() ?? string.Empty; + StateHasChanged(); + } + + private async Task SubmitForm() + { + if (editForm?.EditContext?.Validate() == true) + { + await ChangePassword(); + } + else + { + Snackbar.Add("Vui lòng kiểm tra lại thông tin nhập", Severity.Error); + } + } + + private int GetPasswordStrength() + { + return PasswordStrengthService.EvaluatePasswordStrength(model.NewPassword); + } + + private Color GetPasswordStrengthColor() + { + return PasswordStrengthService.GetStrengthColor(GetPasswordStrength()); + } + + private string GetPasswordStrengthText() + { + return PasswordStrengthService.GetStrengthDescription(GetPasswordStrength()); + } + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + if (authState?.User?.Identity?.IsAuthenticated != true) + { + Snackbar.Add("Vui lòng đăng nhập", Severity.Error); + NavigationManager.NavigateTo("/Account/Login"); + } + } + + private async Task ChangePassword() + { + isProcessing = true; + errorMessage = string.Empty; + try + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = await UserManager.GetUserAsync(authState.User); + + if (user == null) + { + Snackbar.Add("Không tìm thấy thông tin người dùng", Severity.Error); + return; + } + + var result = await UserManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword); + if (result.Succeeded) + { + Snackbar.Add("Đổi mật khẩu thành công", Severity.Success); + model = new ChangePasswordModel(); + isButtonDisabled = true; + } + else + { + errorMessage = string.Join(", ", result.Errors.Select(e => e.Description)); + Snackbar.Add(errorMessage, Severity.Error); + } + } + catch (Exception ex) + { + errorMessage = $"Lỗi: {ex.Message}"; + Snackbar.Add(errorMessage, Severity.Error); + } + finally + { + isProcessing = false; + StateHasChanged(); + } + } + + private void Cancel() + { + model = new ChangePasswordModel(); + isButtonDisabled = true; + errorMessage = string.Empty; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Password.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/Password.razor.css new file mode 100644 index 0000000..a910b03 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Password.razor.css @@ -0,0 +1,566 @@ +.password-container { + padding: 1rem; + min-height: 90vh; + display: flex; + align-items: center; + justify-content: center; + /*background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);*/ + position: relative; + overflow: hidden; +} + + .password-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url('data:image/svg+xml,'); + pointer-events: none; + } + +.password-wrapper { + width: 100%; + max-width: 650px; + position: relative; + z-index: 1; + margin: 0 auto; +} + +.password-card { + border-radius: 24px; + background: rgba(255, 255, 255, 0.98); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 32px 64px rgba(0, 0, 0, 0.12), 0 16px 32px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6); + overflow: hidden; + width: 100%; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + + .password-card:hover { + transform: translateY(-8px); + box-shadow: 0 48px 96px rgba(0, 0, 0, 0.18), 0 24px 48px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.6); + } + +.password-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 3rem 2.5rem; + position: relative; + overflow: hidden; +} + + .password-header::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%); + animation: shimmer 4s ease-in-out infinite; + } + + .password-header::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + } + +@keyframes shimmer { + 0%, 100% { + transform: translateX(-100%) translateY(-100%) rotate(0deg); + } + + 50% { + transform: translateX(20%) translateY(20%) rotate(180deg); + } +} + +.header-content { + display: flex; + align-items: center; + gap: 2.5rem; + width: 100%; + position: relative; + z-index: 2; +} + +.header-icon { + font-size: 3.5rem !important; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + } + + 50% { + transform: scale(1.05); + } +} + +.header-text h4 { + font-weight: 700 !important; + margin-bottom: 0.75rem !important; + color: white !important; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + font-size: 2.25rem !important; + line-height: 1.2; +} + +.header-text .mud-typography-body1 { + color: rgba(255, 255, 255, 0.95) !important; + line-height: 1.6; + font-weight: 400; + font-size: 1.125rem; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} + +.card-content { + padding: 3rem 2.5rem !important; + background: linear-gradient(180deg, #ffffff 0%, #fafbff 100%); + position: relative; +} + +.form-fields { + display: flex; + flex-direction: column; + gap: 2.5rem; +} + +.form-group { + position: relative; + width: 100%; + animation: slideInUp 0.6s ease-out; +} + + .form-group:nth-child(1) { + animation-delay: 0.1s; + } + + .form-group:nth-child(2) { + animation-delay: 0.2s; + } + + .form-group:nth-child(3) { + animation-delay: 0.3s; + } + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(30px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Enhanced TextField Styling */ +.password-field :deep(.mud-input-outlined .mud-input-root) { + border-radius: 16px !important; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 2px solid rgba(102, 126, 234, 0.2) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06); + min-height: 64px; +} + +.password-field :deep(.mud-input-outlined:hover .mud-input-root:not(.mud-input-error)) { + border-color: #667eea !important; + background: rgba(255, 255, 255, 1); + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.15); +} + +.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-root:not(.mud-input-error)) { + border-color: #667eea !important; + background: white; + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1), 0 8px 32px rgba(102, 126, 234, 0.2); + transform: translateY(-3px); +} + +.password-field :deep(.mud-input-label) { + font-weight: 600 !important; + color: #4a5568 !important; + font-size: 1rem; +} + +.password-field :deep(.mud-input-outlined.mud-input-focused .mud-input-label) { + color: #667eea !important; +} + +.password-field :deep(.mud-input-control) { + padding: 0 1rem; + font-size: 1rem; + font-weight: 500; +} + +.password-field :deep(.mud-input-adornment-end) { + margin-right: 0.5rem; +} + +.validation-message { + color: #e53e3e; + font-size: 0.875rem; + font-weight: 500; + margin-top: 0.5rem; + margin-left: 0.75rem; + animation: slideInUp 0.3s ease-out; +} + +/* Password Strength Section */ +.password-strength-container { + background: linear-gradient(135deg, #f8faff 0%, #ffffff 100%); + border-radius: 20px; + padding: 2rem; + margin-top: 1.5rem; + border: 1px solid rgba(102, 126, 234, 0.15); + animation: slideInUp 0.4s ease-out; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); + position: relative; + overflow: hidden; +} + + .password-strength-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #667eea, #764ba2); + border-radius: 20px 20px 0 0; + } + + .password-strength-container .mud-typography-body2 { + font-weight: 600 !important; + color: #2d3748 !important; + margin-bottom: 1rem !important; + font-size: 1rem; + } + + .password-strength-container :deep(.mud-progress-linear) { + height: 12px !important; + border-radius: 8px !important; + margin-bottom: 1.5rem !important; + background-color: #e2e8f0 !important; + overflow: hidden; + position: relative; + } + + .password-strength-container :deep(.mud-progress-linear-bar) { + border-radius: 8px !important; + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1) !important; + position: relative; + } + + .password-strength-container :deep(.mud-progress-linear-bar::after) { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%); + animation: shine 2s ease-in-out infinite; + } + +@keyframes shine { + 0% { + transform: translateX(-100%); + } + + 50% { + transform: translateX(100%); + } + + 100% { + transform: translateX(100%); + } +} + +.password-requirements { + margin-top: 1.5rem; +} + + .password-requirements .mud-typography-caption { + font-weight: 600 !important; + color: #2d3748 !important; + margin-bottom: 1rem !important; + display: block; + font-size: 0.9375rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } + +.requirements-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 1rem; +} + +.requirement { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.875rem; + padding: 0.5rem 0.75rem; + border-radius: 16px; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + font-weight: 500; + border: 2px solid transparent; + position: relative; + overflow: hidden; +} + + .requirement::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); + transition: left 0.4s ease; + } + + .requirement.valid { + color: #22543d; + background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%); + border-color: #68d391; + animation: checkmark 0.5s ease-in-out; + transform: scale(1.02); + box-shadow: 0 4px 16px rgba(72, 187, 120, 0.2); + } + + .requirement.valid::before { + left: 100%; + } + + .requirement.invalid { + color: #718096; + background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%); + border-color: #e2e8f0; + } + + .requirement :deep(.mud-icon-root) { + font-size: 1.25rem !important; + transition: all 0.3s ease; + } + + .requirement.valid :deep(.mud-icon-root) { + animation: bounce 0.6s ease; + } + +@keyframes checkmark { + 0% { + transform: scale(0.8) rotate(-5deg); + opacity: 0.7; + } + + 50% { + transform: scale(1.1) rotate(2deg); + } + + 100% { + transform: scale(1.02) rotate(0deg); + opacity: 1; + } +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + + 40% { + transform: translateY(-4px); + } + + 60% { + transform: translateY(-2px); + } +} + +/* Password Match Indicator */ +.password-match-indicator { + margin-top: 1rem; + animation: slideInUp 0.3s ease-out; +} + + .password-match-indicator .mud-typography { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.9375rem; + padding: 1rem 1.25rem; + border-radius: 16px; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border: 2px solid transparent; + } + + .password-match-indicator .mud-success { + background: linear-gradient(135deg, #f0fff4 0%, #c6f6d5 100%); + border-color: #68d391; + color: #22543d !important; + box-shadow: 0 4px 16px rgba(72, 187, 120, 0.15); + } + + .password-match-indicator .mud-error { + background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%); + border-color: #fc8181; + color: #742a2a !important; + box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15); + } + +.card-actions { + display:flex; + padding: 2rem 2.5rem 2.5rem !important; + background: linear-gradient(180deg, #fafbff 0%, #f4f6ff 100%); + border-top: 1px solid rgba(102, 126, 234, 0.1); + gap: 1.5rem; + justify-content: end; +} + +.action-button { + min-width: 140px !important; + height: 56px !important; + border-radius: 16px !important; + font-weight: 600 !important; + font-size: 1rem !important; + text-transform: none !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important; + position: relative; + overflow: hidden; +} + + .action-button::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s ease; + } + + .action-button:hover::before { + left: 100%; + } + + .action-button:hover { + transform: translateY(-3px) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important; + } + + .action-button:active { + transform: translateY(-1px) !important; + } + + .action-button:disabled { + opacity: 0.6 !important; + transform: none !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05) !important; + } + +/* Error Alert */ +.mud-alert { + border-radius: 16px !important; + border: 2px solid rgba(245, 101, 101, 0.2) !important; + background: linear-gradient(135deg, #fed7d7 0%, #feb2b2 100%) !important; + color: #742a2a !important; + font-weight: 500 !important; + box-shadow: 0 4px 16px rgba(245, 101, 101, 0.15) !important; + margin-top: 1.5rem !important; + animation: slideInUp 0.3s ease-out; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .password-container { + padding: 0.5rem; + } + + .password-wrapper { + max-width: 100%; + } + + .password-header { + padding: 2rem 1.5rem; + } + + .header-content { + flex-direction: column; + gap: 1.5rem; + text-align: center; + } + + .header-icon { + font-size: 2.5rem !important; + } + + .header-text h4 { + font-size: 1.75rem !important; + } + + .card-content { + padding: 2rem 1.5rem !important; + } + + .form-fields { + gap: 2rem; + } + + .password-strength-container { + padding: 1.5rem; + } + + .requirements-list { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .card-actions { + padding: 1.5rem !important; + flex-direction: column; + } + + .action-button { + width: 100% !important; + min-width: unset !important; + } +} + +@media (max-width: 480px) { + .password-header { + padding: 1.5rem 1rem; + } + + .card-content { + padding: 1.5rem 1rem !important; + } + + .password-strength-container { + padding: 1rem; + } + + .card-actions { + padding: 1rem !important; + } +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Register.razor b/RobotNet.IdentityServer/Components/Account/Pages/Register.razor new file mode 100644 index 0000000..1a7bd82 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Register.razor @@ -0,0 +1,120 @@ +@page "/Account/Register" + +@using System.ComponentModel.DataAnnotations +@using System.Text +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Authentication +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.WebUtilities +@using RobotNet.IdentityServer.Data + +@inject UserManager UserManager +@inject IUserStore UserStore +@inject SignInManager SignInManager +@inject IEmailSender EmailSender +@inject ILogger Logger +@inject NavigationManager NavigationManager +@inject IdentityRedirectManager RedirectManager + + +Register + +
+

Create a new account.

+ @if (!string.IsNullOrEmpty(errorMessage)) + { + var statusMessageClass = errorMessage.StartsWith("Error") ? "danger" : "success"; + + } + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
+
+ + +@code { + private string errorMessage = string.Empty; + + private IEnumerable? identityErrors; + + [SupplyParameterFromForm] + private InputModel Input { get; set; } = new(); + + [SupplyParameterFromQuery] + private string? ReturnUrl { get; set; } + + private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; + + public async Task RegisterUser(EditContext editContext) + { + var user = CreateUser(); + + await UserStore.SetUserNameAsync(user, Input.UserName, CancellationToken.None); + user.NormalizedUserName = Input.UserName.ToUpperInvariant(); + user.EmailConfirmed = true; + var result = await UserManager.CreateAsync(user, Input.Password); + + if (!result.Succeeded) + { + identityErrors = result.Errors; + return; + } + + Logger.LogInformation("User created a new account with password."); + + await SignInManager.SignInAsync(user, isPersistent: false); + RedirectManager.RedirectTo(ReturnUrl); + } + + private ApplicationUser CreateUser() + { + try + { + return Activator.CreateInstance(); + } + catch + { + throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + + $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor."); + } + } + + + private sealed class InputModel + { + [Required] + [Display(Name = "UserName")] + public string UserName { get; set; } = ""; + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } = ""; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = ""; + } +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Role.razor b/RobotNet.IdentityServer/Components/Account/Pages/Role.razor new file mode 100644 index 0000000..5680919 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Role.razor @@ -0,0 +1,923 @@ +@page "/Account/Rolemanager" + +@rendermode InteractiveServer +@attribute [Authorize] + +@using Microsoft.AspNetCore.Authorization +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Identity +@using Microsoft.AspNetCore.Components +@using RobotNet.IdentityServer.Data +@using Microsoft.EntityFrameworkCore +@using System.Threading + +@inherits LayoutComponentBase + +@inject AuthenticationStateProvider AuthenticationStateProvider +@inject UserManager UserManager +@inject RoleManager RoleManager +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + + + + + +
+
+ + +
+ +
+ Role Management + Quản lý vai trò và phân quyền người dùng +
+
+
+ +
+ @Roles.Count + Tổng số vai trò +
+
+
+
+ + + + +
+ + Danh Sách Vai Trò +
+ +
+ +
+ +
+ + Tạo Vai Trò Mới + +
+ +
+ + + Tên Vai Trò + Thao Tác + + + +
+ + @context.Name +
+
+ + @if (LoggedInUserRoles.Contains(context.Name ?? string.Empty)) + { + + + + } + else + { + + + + + + + } + +
+ + + +
+
+
+
+ + + + +
+ + Quản Lý Người Dùng +
+ +
+ +
+ +
+ + + Người Dùng + Vai Trò + Thao Tác + + + +
+
+ @(context.UserName?.Substring(0, 1).ToUpper()) +
+
+ @context.UserName + @if (context.UserId == LoggedInUserId) + { + + Bạn + + } +
+
+
+ +
+ @if (context.Roles.Any()) + { + foreach (var role in context.Roles) + { + + @role + + } + } + else + { + + Chưa có vai trò + + } +
+
+ + @if (context.UserId == LoggedInUserId) + { + + + + } + else + { + + + + } + +
+ + + +
+
+
+
+
+
+ + + + +
+ + Tạo Vai Trò Mới +
+
+ + + + + + Tạo + + + Hủy + + +
+ + + + +
+ + Chỉnh Sửa Vai Trò +
+
+ + + + + + Lưu + + + Hủy + + +
+ + + + +
+ + Xác Nhận Xóa +
+
+ + + Bạn có chắc chắn muốn xóa vai trò @RoleNameToDelete? + + Hành động này không thể hoàn tác. + + + + Xóa + + + Hủy + + +
+ + + + +
+ + Quản Lý Vai Trò: @SelectedUserName +
+
+ + + + + + + Vai Trò Hiện Tại + +
+ @if (AssignedRoles.Any()) + { + foreach (var role in AssignedRoles) + { + if (role.Equals("Administrator", StringComparison.OrdinalIgnoreCase)) + { + + @role (Được bảo vệ) + + } + else + { + + @role + + } + } + } + else + { + + Chưa có vai trò nào + + } +
+
+
+ + + + + + + Thêm Vai Trò + + + @foreach (var role in AvailableRoles) + { + +
+ + @role +
+
+ } +
+ + Thêm (@selectedRolesToAdd.Count()) + +
+
+ + + @{ + var removableRoles = AssignedRoles + .Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + @if (removableRoles.Any()) + { + + + + + Xóa Vai Trò + + + @foreach (var role in removableRoles) + { + +
+ + @role +
+
+ } +
+ + Xóa (@selectedRolesToRemove.Count()) + +
+
+ } +
+
+ + + Đóng + + +
+ +@code { + + private bool CreateRoleVisible { get; set; } + private bool DelRoleVisible { get; set; } + private bool EditRoleVisible { get; set; } + private bool ManageUserRolesVisible { get; set; } = false; + + private string CurrentRoleId { get; set; } = ""; + private string EditRoleName { get; set; } = ""; + private string NewRoleName { get; set; } = ""; + private string RoleNameToDelete { get; set; } = ""; + private string RoleIdToDelete { get; set; } = ""; + + private string SelectedUserName { get; set; } = string.Empty; + private string UserIdToManageRoles { get; set; } = string.Empty; + + private string LoggedInUserId { get; set; } = string.Empty; + private List LoggedInUserRoles { get; set; } = new(); + private List AllRoles { get; set; } = new List(); + private List AvailableRoles { get; set; } = new List(); + private List AssignedRoles { get; set; } = new List(); + private List Roles { get; set; } = new List(); + private List UsersWithRoles { get; set; } = new List(); + + + private IEnumerable selectedRolesToAdd = new HashSet(); + private IEnumerable selectedRolesToRemove = new HashSet(); + + + private string rowsPerPageString = "Rows:"; + private string roleSearchTerm = ""; + private string userSearchTerm = ""; + + private IEnumerable FilteredRoles => + string.IsNullOrWhiteSpace(roleSearchTerm) + ? Roles + : Roles?.Where(r => r.Name != null && r.Name.Contains(roleSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty(); + + private IEnumerable FilteredUsers => + string.IsNullOrWhiteSpace(userSearchTerm) + ? UsersWithRoles + : UsersWithRoles?.Where(u => u.UserName != null && u.UserName.Contains(userSearchTerm, StringComparison.OrdinalIgnoreCase)) ?? Enumerable.Empty(); + + + private string GetRoleIcon(string roleName) + { + return roleName?.ToLower() switch + { + "administrator" => Icons.Material.Filled.SupervisorAccount, + "user" => Icons.Material.Filled.Person, + "guest" => Icons.Material.Filled.PersonOutline, + _ => Icons.Material.Filled.Security + }; + } + + private Color GetRoleColor(string roleName) + { + return roleName?.ToLower() switch + { + "administrator" => Color.Error, + "user" => Color.Primary, + "guest" => Color.Default, + _ => Color.Info + }; + } + + private Color GetRoleChipColor(string roleName) + { + return roleName?.ToLower() switch + { + "administrator" => Color.Error, + "user" => Color.Primary, + "guest" => Color.Default, + _ => Color.Info + }; + } + + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var currentUser = authenticationState.User; + + if (currentUser.Identity?.IsAuthenticated == true) + { + LoggedInUserId = currentUser.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? string.Empty; + + var currentUserObj = await UserManager.GetUserAsync(currentUser); + if (currentUserObj != null) + { + LoggedInUserRoles = (await UserManager.GetRolesAsync(currentUserObj)).ToList(); + } + Roles = await RoleManager.Roles.OrderBy(r => r.CreatedDate).ToListAsync(); + await LoadAllRoles(); + await LoadUsersWithRoles(); + StateHasChanged(); + } + else + { + Snackbar.Add("User is not authenticated.", Severity.Error); + } + } + + private async Task LoadUsersWithRoles() + { + try + { + var users = await UserManager.Users.ToListAsync(); + if (users == null || !users.Any()) + { + Snackbar.Add("No users found.", Severity.Error); + return; + } + + var userRoleList = new List(); + + foreach (var user in users) + { + var userRoles = await UserManager.GetRolesAsync(user); + + userRoleList.Add(new UserRoleModel + { + UserName = user.UserName, + Roles = userRoles.ToList(), + UserId = user.Id + }); + } + + UsersWithRoles = userRoleList.OrderBy(u => u.UserId == LoggedInUserId ? 0 : 1).ToList(); + StateHasChanged(); + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải người dùng và role: {ex.Message}", Severity.Error); + UsersWithRoles = new List(); + } + } + + private async Task LoadAllRoles() + { + try + { + var roles = await RoleManager.Roles + .Select(role => role.Name) + .ToListAsync(); + + AllRoles = roles.Where(name => !string.IsNullOrEmpty(name)).Cast().ToList() ?? new List(); + + if (!AllRoles.Any()) + { + Snackbar.Add("Không tìm thấy vai trò nào.", Severity.Warning); + } + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải danh sách vai trò: {ex.Message}", Severity.Error); + AllRoles = new List(); + } + } + + private void AddRole() + { + CreateRoleVisible = true; + StateHasChanged(); + } + + private void DelRole(string roleId, string roleName) + { + RoleIdToDelete = roleId; + RoleNameToDelete = roleName; + DelRoleVisible = true; + StateHasChanged(); + } + + private void EditRole(ApplicationRole role) + { + if (role?.Name is not null) + { + CurrentRoleId = role.Id; + EditRoleName = role.Name; + EditRoleVisible = true; + StateHasChanged(); + } + else + { + Snackbar.Add("Role information is incomplete or invalid.", Severity.Error); + } + } + + private async Task ManageUserRoles(string userId, string userName) + { + + if (!LoggedInUserRoles.Contains("Administrator")) + { + Snackbar.Add("Bạn không có quyền quản lý role của user khác.", Severity.Error); + return; + } + + if (!AllRoles.Any()) + { + Snackbar.Add("Không có vai trò nào có thể chỉ định", Severity.Warning); + return; + } + + SelectedUserName = userName; + UserIdToManageRoles = userId; + + var user = await UserManager.FindByIdAsync(userId); + if (user != null) + { + var userRoles = await UserManager.GetRolesAsync(user); + AssignedRoles = userRoles.ToList(); + + AvailableRoles = AllRoles + .Except(userRoles) + .Where(role => !role.Equals("Administrator", StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + selectedRolesToAdd = new HashSet(); + selectedRolesToRemove = new HashSet(); + + ManageUserRolesVisible = true; + StateHasChanged(); + } + + private async Task AddSelectedRolesToUser() + { + foreach (var role in selectedRolesToAdd.ToList()) + { + await AddRoleToSelectedUser(role); + } + selectedRolesToAdd = new HashSet(); + StateHasChanged(); + } + + private async Task RemoveSelectedRolesFromUser() + { + foreach (var role in selectedRolesToRemove.ToList()) + { + await RemoveRoleFromSelectedUser(role); + } + selectedRolesToRemove = new HashSet(); + StateHasChanged(); + } + + private async Task AddRoleToSelectedUser(string roleName) + { + if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase)) + { + Snackbar.Add("Không thể gán role Admin cho user khác.", Severity.Error); + return; + } + + + if (!LoggedInUserRoles.Contains("Administrator")) + { + Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error); + return; + } + + var user = await UserManager.FindByIdAsync(UserIdToManageRoles); + if (user != null) + { + var result = await UserManager.AddToRoleAsync(user, roleName); + + if (result.Succeeded) + { + AssignedRoles.Add(roleName); + AvailableRoles.Remove(roleName); + + selectedRolesToAdd = selectedRolesToAdd.Where(r => r != roleName); + + Snackbar.Add($"Thêm role '{roleName}' cho {SelectedUserName} Thành Công.", Severity.Success); + await LoadUsersWithRoles(); + StateHasChanged(); + } + else + { + Snackbar.Add($"Failed to add role '{roleName}' to user.", Severity.Error); + } + } + } + + private async Task RemoveRoleFromSelectedUser(string roleName) + { + if (roleName.Equals("Administrator", StringComparison.OrdinalIgnoreCase)) + { + Snackbar.Add("Không thể xóa role Admin của user.", Severity.Error); + return; + } + + + if (!LoggedInUserRoles.Contains("Administrator")) + { + Snackbar.Add("Bạn không có quyền thực hiện thao tác này.", Severity.Error); + return; + } + var user = await UserManager.FindByIdAsync(UserIdToManageRoles); + if (user != null) + { + var result = await UserManager.RemoveFromRoleAsync(user, roleName); + + if (result.Succeeded) + { + AssignedRoles.Remove(roleName); + AvailableRoles.Add(roleName); + selectedRolesToRemove = selectedRolesToRemove.Where(r => r != roleName); + + Snackbar.Add($"Thành công xoá role '{roleName}' của {SelectedUserName}.", Severity.Success); + await LoadUsersWithRoles(); + StateHasChanged(); + } + else + { + Snackbar.Add($"Failed to remove role '{roleName}' from user.", Severity.Error); + } + } + } + + private async Task CreateRole() + { + if (string.IsNullOrWhiteSpace(NewRoleName)) + { + Snackbar.Add(" Tên Role không được để trống.", Severity.Error); + StateHasChanged(); + return; + } + + var roleExist = await RoleManager.RoleExistsAsync(NewRoleName.ToUpper()); + if (roleExist) + { + Snackbar.Add(" Role đã tồn tại.", Severity.Warning); + CreateRoleVisible = false; + NewRoleName = ""; + StateHasChanged(); + return; + } + + var newRole = new ApplicationRole + { + Name = NewRoleName, + NormalizedName = NewRoleName.ToUpper(), + CreatedDate = DateTime.UtcNow + }; + + var result = await RoleManager.CreateAsync(newRole); + + if (result.Succeeded) + { + Roles.Add(newRole); + Snackbar.Add(" Tạo Role thành công!", Severity.Success); + await LoadAllRoles(); + CreateRoleVisible = false; + NewRoleName = ""; + StateHasChanged(); + } + else + { + Snackbar.Add(" Tạo Role thất bại.", Severity.Error); + StateHasChanged(); + } + } + + private async Task SaveEditRole() + { + if (string.IsNullOrWhiteSpace(EditRoleName)) + { + Snackbar.Add("Tên Role không được để trống.", Severity.Error); + StateHasChanged(); + return; + } + + var role = await RoleManager.FindByIdAsync(CurrentRoleId); + if (role != null) + { + role.Name = EditRoleName; + role.NormalizedName = EditRoleName.ToUpper(); + + var result = await RoleManager.UpdateAsync(role); + + if (result.Succeeded) + { + var existingRole = Roles.FirstOrDefault(r => r.Id == role.Id); + if (existingRole != null) + { + existingRole.Name = role.Name; + } + + Snackbar.Add("Role đã được sửa thành công!", Severity.Success); + await LoadAllRoles(); + await LoadUsersWithRoles(); + EditRoleVisible = false; + EditRoleName = ""; + StateHasChanged(); + } + else + { + Snackbar.Add("Sửa Role thất bại.", Severity.Error); + EditRoleVisible = false; + StateHasChanged(); + } + } + else + { + Snackbar.Add("Không tìm thấy role với ID đã cho.", Severity.Error); + EditRoleVisible = false; + StateHasChanged(); + } + } + + private async Task ConfirmDelRole() + { + if (string.IsNullOrEmpty(RoleIdToDelete)) + { + Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error); + return; + } + + var role = await RoleManager.FindByIdAsync(RoleIdToDelete); + if (role != null) + { + var result = await RoleManager.DeleteAsync(role); + if (result.Succeeded) + { + Snackbar.Add(" Đã xóa Role thành công.", Severity.Success); + Roles = await RoleManager.Roles + .OrderBy(r => r.CreatedDate) + .ToListAsync(); + await LoadAllRoles(); + await LoadUsersWithRoles(); + } + else + { + Snackbar.Add(" Xóa Role thất bại.", Severity.Error); + } + } + else + { + Snackbar.Add(" Không tìm thấy Role để xóa.", Severity.Error); + } + + DelRoleVisible = false; + RoleIdToDelete = ""; + StateHasChanged(); + } + + public class UserRoleModel + { + public string? UserName { get; set; } + public List Roles { get; set; } = new List(); + public string? UserId { get; set; } + } + +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/Role.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/Role.razor.css new file mode 100644 index 0000000..58709ee --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/Role.razor.css @@ -0,0 +1,87 @@ +.mdi { + display: inline-block; + position: relative; + background-size: cover; + align-items:center; +} +.pa-4{ + overflow:hidden; +} +.role-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 12px; + padding: 24px; + margin-bottom: 24px; + box-shadow: 0 8px 32px rgba(0,0,0,0.12); +} + +.modern-card { + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0,0,0,0.08); + border: 1px solid rgba(0,0,0,0.06); + transition: all 0.3s ease; +} + + .modern-card:hover { + box-shadow: 0 8px 40px rgba(0,0,0,0.12); + transform: translateY(-2px); + } + +.section-title { + font-weight: 600; + color: #2d3748; + margin-bottom: 16px; + display: flex; + align-items: center; + gap: 8px; +} + +.stats-card { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border-radius: 12px; + padding: 20px; + text-align: center; + box-shadow: 0 4px 20px rgba(0,0,0,0.1); +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + margin-right: 12px; +} + +.role-chip { + margin: 2px; + font-size: 12px; +} + +.action-button { + border-radius: 8px; + transition: all 0.2s ease; +} + + .action-button:hover { + transform: scale(1.05); + } + +.table-container { + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0,0,0,0.06); +} + +.search-container { + background: #f8fafc; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor b/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor new file mode 100644 index 0000000..3c3c1b1 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor @@ -0,0 +1,17 @@ +@page "/Account/Usermanager" + +@rendermode InteractiveServer + + + + + + + + + + + +@code { + +} diff --git a/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor.css b/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor.css new file mode 100644 index 0000000..3ce0a4e --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/UserManager.razor.css @@ -0,0 +1,73 @@ +.mdi { + display: inline-flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} +.user-manager-container { + min-height: 100vh; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background-attachment: fixed; +} + +.header-section { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.modern-tabs { + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(20px); + border-radius: 20px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); + padding: 24px; + border: 1px solid rgba(255, 255, 255, 0.3); +} + + .modern-tabs .mud-tabs-toolbar { + background: transparent; + border-radius: 12px; + padding: 8px; + margin-bottom: 24px; + background: rgba(21, 101, 192, 0.05); + } + + .modern-tabs .mud-tab { + border-radius: 8px; + margin: 0 4px; + transition: all 0.3s ease; + font-weight: 500; + } + + .modern-tabs .mud-tab:hover { + background: rgba(21, 101, 192, 0.1); + transform: translateY(-2px); + } + + .modern-tabs .mud-tab.mud-tab-active { + background: rgba(21, 101, 192, 0.15); + color: #1565C0; + font-weight: 600; + } + +.tab-content { + animation: fadeInUp 0.5s ease-out; +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.tab-panel { + padding: 0 !important; +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Account/Pages/_Imports.razor b/RobotNet.IdentityServer/Components/Account/Pages/_Imports.razor new file mode 100644 index 0000000..0b19572 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Pages/_Imports.razor @@ -0,0 +1,2 @@ +@using RobotNet.IdentityServer.Components.Account.Shared +@attribute [ExcludeFromInteractiveRouting] diff --git a/RobotNet.IdentityServer/Components/Account/Shared/RedirectToLogin.razor b/RobotNet.IdentityServer/Components/Account/Shared/RedirectToLogin.razor new file mode 100644 index 0000000..c8b8eff --- /dev/null +++ b/RobotNet.IdentityServer/Components/Account/Shared/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager NavigationManager + +@code { + protected override void OnInitialized() + { + NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); + } +} diff --git a/RobotNet.IdentityServer/Components/App.razor b/RobotNet.IdentityServer/Components/App.razor new file mode 100644 index 0000000..2f7a981 --- /dev/null +++ b/RobotNet.IdentityServer/Components/App.razor @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RobotNet.IdentityServer/Components/Layout/MainLayout.razor b/RobotNet.IdentityServer/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..046cc22 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Layout/MainLayout.razor @@ -0,0 +1,29 @@ +@using MudBlazor + +@inherits LayoutComponentBase + +@inject NavigationManager NavigationManager + + + + + +
+ + +
+
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
\ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css b/RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..6f24a5d --- /dev/null +++ b/RobotNet.IdentityServer/Components/Layout/MainLayout.razor.css @@ -0,0 +1,123 @@ +.page { + position: relative; + display: flex; + flex-direction: column; + background-color: #f7f7f7; + min-height: 100vh; +} +.content px-2{ + overflow:hidden; +} +main { + flex: 1; + padding-left: 1rem; +} + +.sidebar-container { + position: relative; + z-index: 10; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); + border-radius: 15px; + box-shadow: 4px 4px 12px rgba(0, 0, 0, 0.3); + transform: translateX(5px); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } + + .sidebar { + margin-left: 0; + width: 80%; + max-width: 250px; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar-container { + width: 280px; + height: 100vh; + + } + + .sidebar { + width: 270px; + height: calc(100vh - 20px); + position: fixed; + top:10px; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 0.75rem !important; + padding-right: 0.75rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/RobotNet.IdentityServer/Components/Layout/NavMenu.razor b/RobotNet.IdentityServer/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..92561cb --- /dev/null +++ b/RobotNet.IdentityServer/Components/Layout/NavMenu.razor @@ -0,0 +1,214 @@ +@using Microsoft.AspNetCore.Identity +@using RobotNet.IdentityServer.Data +@using MudBlazor +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components +@using Microsoft.EntityFrameworkCore +@using System.Threading +@using RobotNet.IdentityServer.Services +@using System.Security.Claims + +@implements IDisposable +@inherits LayoutComponentBase + +@inject RobotNet.IdentityServer.Services.IdentityService IdentityService +@inject RobotNet.IdentityServer.Services.UserInfoService UserInfoService + +@inject NavigationManager NavigationManager +@inject AuthenticationStateProvider AuthenticationStateProvider + +@inject RoleManager RoleManager + + + + +
+ User Management + +
+ + +@code { + private Func? _userInfoChangedHandler; + private string cacheBuster = ""; + private string? currentUrl; + private ApplicationUser? currentUser; + + @inject UserManager UserManager + @inject AuthenticationStateProvider AuthenticationStateProvider + + private string userName = string.Empty; + private string userEmail = string.Empty; + private string userImageUrl = string.Empty; + + protected override async Task OnInitializedAsync() + { + + currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); + NavigationManager.LocationChanged += OnLocationChanged; + + _userInfoChangedHandler = async () => + { + await InvokeAsync(async () => + { + await LoadUserInfoAsync(); + StateHasChanged(); + }); + }; + UserInfoService.RegisterHandler(_userInfoChangedHandler); + await LoadUserInfoAsync(); + } + + private async Task UserInfoChangedHandler() + { + await LoadUserInfoAsync(); + + + await InvokeAsync(() => + { + cacheBuster = $"?v={DateTime.Now.Ticks}"; + StateHasChanged(); + }); + } + + private async Task LoadUserInfoAsync() + { + try + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user?.Identity?.IsAuthenticated == true) + { + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); + if (!string.IsNullOrEmpty(userId)) + { + + currentUser = await IdentityService.GetUserByIdAsync(userId); + if (currentUser != null) + { + userName = currentUser.UserName ?? string.Empty; ; + userEmail = currentUser.Email ?? string.Empty; ; + + if (currentUser.AvatarImage != null) + { + + userImageUrl = $"data:{currentUser.AvatarContentType ?? "image/jpeg"};base64,{Convert.ToBase64String(currentUser.AvatarImage)}"; + } + else + { + + userImageUrl = "/uploads/avatars/anh.jpg"; + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error loading user info: {ex.Message}"); + } + } + + + + private void OnLocationChanged(object? sender, LocationChangedEventArgs e) + { + InvokeAsync(() => + { + currentUrl = NavigationManager.ToBaseRelativePath(e.Location); + StateHasChanged(); + return Task.CompletedTask; + }); + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + + if (UserInfoService != null && _userInfoChangedHandler != null) + { + UserInfoService.UnregisterHandler(_userInfoChangedHandler); + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css b/RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..3eb830f --- /dev/null +++ b/RobotNet.IdentityServer/Components/Layout/NavMenu.razor.css @@ -0,0 +1,202 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: turquoise; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + + .navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); + } + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.1); + border-radius: 15px 15px 0 0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.mdi { + display: inline-block; + position: relative; + font-size: 26px; + background-size: cover; +} + +.nav-item { + font-size: 1.05rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 0.5rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #4a5568; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + transition: all 0.2s ease; + } + + .nav-item ::deep a.active { + background-color: rgba(79, 70, 229, 0.2); + color: #4338ca; + } + + .nav-item ::deep .nav-link:hover { + background-color: rgba(79, 70, 229, 0.1); + color: #4338ca; + } +.text-nav { + margin-left: 1rem; +} + +.nav-scrollable { + display: none; + display: flex; + flex-direction: column; + height: calc(100% - 3.5rem); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + + + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + + +.user-profile { + margin-top: auto; + padding: 1rem; + border-top: 1px solid rgba(0, 0, 0, 0.1); + background-color: #e8f0fe; + border-radius: 0 0 15px 15px; +} + +.user-profile-inner { + display: flex; + align-items: center; + padding: 0.5rem; + position: relative; +} + +.avatar { + width: 40px; + height: 40px; + min-width: 40px; + min-height: 40px; + max-width: 40px; + max-height: 40px; + border-radius: 50%; + overflow: hidden; + margin-right: 0.75rem; + background-color: rgba(79, 70, 229, 0.1); + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + flex-shrink: 0; +} + +.avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: #4a5568; +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; +} + +.user-info { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + min-width: 0; +} + +.username { + color: #4a5568; + font-weight: 500; + font-size: 0.9rem; + line-height: 1.2; +} + +.user-email { + color: #718096; + font-size: 0.75rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; +} + +.logout-form { + margin-left: auto; +} + +.logout-button { + background: none; + border: none; + color: #718096; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; +} + + .logout-button:hover { + background-color: rgba(79, 70, 229, 0.1); + color: #4338ca; + } + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: flex; + flex-direction: column; + /* Allow sidebar to scroll for tall menus */ + height: calc(100% - 3.5rem); + overflow-y: auto; + border-radius: 0 0 15px 15px; + background: #e8f0fe; + } +} diff --git a/RobotNet.IdentityServer/Components/Pages/Error.razor b/RobotNet.IdentityServer/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/RobotNet.IdentityServer/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/RobotNet.IdentityServer/Components/Pages/Home.razor b/RobotNet.IdentityServer/Components/Pages/Home.razor new file mode 100644 index 0000000..a1c789a --- /dev/null +++ b/RobotNet.IdentityServer/Components/Pages/Home.razor @@ -0,0 +1,27 @@ +@page "/" +@using Microsoft.AspNetCore.Authorization + +@rendermode InteractiveServer + +@attribute [Authorize] + +Home + + + +

Hello

+ + + + + Vui lòng đăng nhập + + + Hello @context.User.Identity?.Name! + + + + +@code { + +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/Routes.razor b/RobotNet.IdentityServer/Components/Routes.razor new file mode 100644 index 0000000..2d84756 --- /dev/null +++ b/RobotNet.IdentityServer/Components/Routes.razor @@ -0,0 +1,12 @@ +@using RobotNet.IdentityServer.Components.Account.Shared + + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.IdentityServer/Components/_Imports.razor b/RobotNet.IdentityServer/Components/_Imports.razor new file mode 100644 index 0000000..4308c62 --- /dev/null +++ b/RobotNet.IdentityServer/Components/_Imports.razor @@ -0,0 +1,14 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using MudBlazor +@using RobotNet.IdentityServer +@using RobotNet.IdentityServer.Components +@using System.ComponentModel.DataAnnotations \ No newline at end of file diff --git a/RobotNet.IdentityServer/Controllers/AuthorizationController.cs b/RobotNet.IdentityServer/Controllers/AuthorizationController.cs new file mode 100644 index 0000000..d44a06a --- /dev/null +++ b/RobotNet.IdentityServer/Controllers/AuthorizationController.cs @@ -0,0 +1,402 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using RobotNet.IdentityServer.Data; +using RobotNet.IdentityServer.Helpers; +using System.Security.Claims; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace RobotNet.IdentityServer.Controllers; + +[EnableCors("RequestAuthorize")] +[Route("api/[controller]")] +[ApiController] +public class AuthorizationController( + IOpenIddictApplicationManager applicationManager, + IOpenIddictAuthorizationManager authorizationManager, + IOpenIddictScopeManager scopeManager, + SignInManager signInManager, + UserManager userManager) : ControllerBase +{ + [HttpGet("connect/authorize")] + [HttpPost("connect/authorize")] + [IgnoreAntiforgeryToken] + public async Task Authorize() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + // Try to retrieve the user principal stored in the authentication cookie and redirect + // the user agent to the login page (or to an external provider) in the following cases: + // + // - If the user principal can't be extracted or the cookie is too old. + // - If prompt=login was specified by the client application. + // - If a max_age parameter was provided and the authentication cookie is not considered "fresh" enough. + // + // For scenarios where the default authentication handler configured in the ASP.NET Core + // authentication options shouldn't be used, a specific scheme can be specified here. + var result = await HttpContext.AuthenticateAsync(); + if (result == null || !result.Succeeded || request.HasPromptValue(PromptValues.Login) || + (request.MaxAge != null && result.Properties?.IssuedUtc != null && + DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) + { + // If the client application requested promptless authentication, + // return an error indicating that the user is not logged in. + if (request.HasPromptValue(PromptValues.None)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.LoginRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is not logged in." + })); + } + + // To avoid endless login -> authorization redirects, the prompt=login flag + // is removed from the authorization request payload before redirecting the user. + var prompt = string.Join(" ", request.GetPromptValues().Remove(PromptValues.Login)); + + var parameters = Request.HasFormContentType ? + Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() : + Request.Query.Where(parameter => parameter.Key != Parameters.Prompt).ToList(); + + parameters.Add(KeyValuePair.Create(Parameters.Prompt, new StringValues(prompt))); + + // For scenarios where the default challenge handler configured in the ASP.NET Core + // authentication options shouldn't be used, a specific scheme can be specified here. + return Challenge(new AuthenticationProperties + { + RedirectUri = Request.PathBase + Request.Path + QueryString.Create(parameters) + }); + } + + // Retrieve the profile of the logged in user. + var user = await userManager.GetUserAsync(result.Principal) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); + + // Retrieve the application details from the database. + var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ?? + throw new InvalidOperationException("Details concerning the calling client application cannot be found."); + + // Retrieve the permanent authorizations associated with the user and the calling client application. + var authorizations = await authorizationManager.FindAsync( + subject: await userManager.GetUserIdAsync(user), + client: await applicationManager.GetIdAsync(application), + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + switch (await applicationManager.GetConsentTypeAsync(application)) + { + // If the consent is external (e.g when authorizations are granted by a sysadmin), + // immediately return an error if no authorization can be found in the database. + case ConsentTypes.External when authorizations.Count is 0: + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + + // If the consent is implicit or if an authorization was found, + // return an authorization response without displaying the consent form. + case ConsentTypes.Implicit: + case ConsentTypes.External when authorizations.Count is not 0: + case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPromptValue(PromptValues.Consent): + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]); + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + identity.SetScopes(request.GetScopes()); + identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + + // Automatically create a permanent authorization to avoid requiring explicit consent + // for future authorization or token requests containing the same scopes. + var authorization = authorizations.LastOrDefault(); + authorization ??= await authorizationManager.CreateAsync( + identity: identity, + subject: await userManager.GetUserIdAsync(user), + client: await applicationManager.GetIdAsync(application) ?? "", + type: AuthorizationTypes.Permanent, + scopes: identity.GetScopes()); + + identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); + identity.SetDestinations(GetDestinations); + + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // At this point, no authorization was found in the database and an error must be returned + // if the client application specified prompt=none in the authorization request. + case ConsentTypes.Explicit when request.HasPromptValue(PromptValues.None): + case ConsentTypes.Systematic when request.HasPromptValue(PromptValues.None): + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "Interactive user consent is required." + })); + + // In every other case, render the consent form. + default: + return Redirect($"/Account/Login/Access{Request.QueryString}&request_app={await applicationManager.GetLocalizedDisplayNameAsync(application)}&request_scope={request.Scope}"); + } + } + + [Authorize, FormValueRequired("submit.Accept")] + [HttpPost("connect/authorize"), ValidateAntiForgeryToken] + public async Task Accept() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + // Retrieve the profile of the logged in user. + var user = await userManager.GetUserAsync(User) ?? + throw new InvalidOperationException("The user details cannot be retrieved."); + + // Retrieve the application details from the database. + var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? "") ?? + throw new InvalidOperationException("Details concerning the calling client application cannot be found."); + + // Retrieve the permanent authorizations associated with the user and the calling client application. + var authorizations = await authorizationManager.FindAsync( + subject: await userManager.GetUserIdAsync(user), + client: await applicationManager.GetIdAsync(application), + status: Statuses.Valid, + type: AuthorizationTypes.Permanent, + scopes: request.GetScopes()).ToListAsync(); + + // Note: the same check is already made in the other action but is repeated + // here to ensure a malicious user can't abuse this POST-only endpoint and + // force it to return a valid response without the external authorization. + if (authorizations.Count is 0 && await applicationManager.HasConsentTypeAsync(application, ConsentTypes.External)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.ConsentRequired, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The logged in user is not allowed to access this client application." + })); + } + + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens. + identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]); + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + identity.SetScopes(request.GetScopes()); + identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + + // Automatically create a permanent authorization to avoid requiring explicit consent + // for future authorization or token requests containing the same scopes. + var authorization = authorizations.LastOrDefault(); + authorization ??= await authorizationManager.CreateAsync( + identity: identity, + subject: await userManager.GetUserIdAsync(user), + client: await applicationManager.GetIdAsync(application) ?? "", + type: AuthorizationTypes.Permanent, + scopes: identity.GetScopes()); + + identity.SetAuthorizationId(await authorizationManager.GetIdAsync(authorization)); + identity.SetDestinations(GetDestinations); + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + [Authorize, FormValueRequired("submit.Deny")] + [HttpPost("connect/authorize"), ValidateAntiForgeryToken] + // Notify OpenIddict that the authorization grant has been denied by the resource owner + // to redirect the user agent to the client application using the appropriate response_mode. + public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + [HttpGet("connect/logout")] + public IActionResult Logout() => Redirect($"/Account/Logout/Confirm{Request.QueryString}"); + + //[ActionName(nameof(Logout)), HttpPost("connect/logout"), ValidateAntiForgeryToken] + [Authorize, FormValueRequired("submit.Confirm")] + [HttpPost("connect/logout"), ValidateAntiForgeryToken] + public async Task LogoutPost() + { + // Ask ASP.NET Core Identity to delete the local and external cookies created + // when the user agent is redirected from the external identity provider + // after a successful authentication flow (e.g Google or Facebook). + await signInManager.SignOutAsync(); + + // Returning a SignOutResult will ask OpenIddict to redirect the user agent + // to the post_logout_redirect_uri specified by the client application or to + // the RedirectUri specified in the authentication properties if none was set. + return SignOut( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties + { + RedirectUri = "/" + }); + } + + [HttpPost("connect/token"), IgnoreAntiforgeryToken, Produces("application/json")] + public async Task Exchange() + { + var request = HttpContext.GetOpenIddictServerRequest() ?? + throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); + + if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) + { + // Retrieve the claims principal stored in the authorization code/refresh token. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // Retrieve the user profile corresponding to the authorization code/refresh token. + var user = await userManager.FindByIdAsync(result.Principal?.GetClaim(Claims.Subject) ?? ""); + if (user is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + })); + } + + // Ensure the user is still allowed to sign in. + if (!await signInManager.CanSignInAsync(user)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." + })); + } + + var identity = new ClaimsIdentity(result.Principal?.Claims, + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Override the user claims present in the principal in case they + // changed since the authorization code/refresh token was issued. + identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user)) + .SetClaim(Claims.Email, await userManager.GetEmailAsync(user)) + .SetClaim(Claims.Name, await userManager.GetUserNameAsync(user)) + .SetClaim(Claims.PreferredUsername, await userManager.GetUserNameAsync(user)) + .SetClaims(Claims.Role, [.. (await userManager.GetRolesAsync(user))]); + + identity.SetDestinations(GetDestinations); + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + else if (request.IsClientCredentialsGrantType()) + { + // Xử lý Client Credentials Flow + var application = await applicationManager.FindByClientIdAsync(request.ClientId ?? ""); + if (application == null) throw new InvalidOperationException("The application details cannot be found in the database."); + + // Create the claims-based identity that will be used by OpenIddict to generate tokens. + var identity = new ClaimsIdentity( + authenticationType: TokenValidationParameters.DefaultAuthenticationType, + nameType: Claims.Name, + roleType: Claims.Role); + + // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier). + identity.SetClaim(Claims.Subject, await applicationManager.GetClientIdAsync(application)); + identity.SetClaim(Claims.Name, await applicationManager.GetDisplayNameAsync(application)); + + // Note: In the original OAuth 2.0 specification, the client credentials grant + // doesn't return an identity token, which is an OpenID Connect concept. + // + // As a non-standardized extension, OpenIddict allows returning an id_token + // to convey information about the client application when the "openid" scope + // is granted (i.e specified when calling principal.SetScopes()). When the "openid" + // scope is not explicitly set, no identity token is returned to the client application. + + // Set the list of scopes granted to the client application in access_token. + identity.SetScopes(request.GetScopes()); + identity.SetResources(await scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync()); + identity.SetDestinations(GetDestinations); + + return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + throw new InvalidOperationException("The specified grant type is not supported."); + } + + private static IEnumerable GetDestinations(Claim claim) + { + // Note: by default, claims are NOT automatically included in the access and identity tokens. + // To allow OpenIddict to serialize them, you must attach them a destination, that specifies + // whether they should be included in access tokens, in identity tokens or in both. + + switch (claim.Type) + { + case Claims.Name or Claims.PreferredUsername: + yield return Destinations.AccessToken; + + if (claim.Subject?.HasScope(Scopes.Profile) ?? false) + yield return Destinations.IdentityToken; + + yield break; + + case Claims.Email: + yield return Destinations.AccessToken; + + if (claim.Subject?.HasScope(Scopes.Email) ?? false) + yield return Destinations.IdentityToken; + + yield break; + + case Claims.Role: + yield return Destinations.AccessToken; + + if (claim.Subject?.HasScope(Scopes.Roles) ?? false) + yield return Destinations.IdentityToken; + + yield break; + + // Never include the security stamp in the access and identity tokens, as it's a secret value. + case "AspNet.Identity.SecurityStamp": yield break; + + default: + yield return Destinations.AccessToken; + yield break; + } + } +} diff --git a/RobotNet.IdentityServer/Controllers/IdentityServerLoggerController.cs b/RobotNet.IdentityServer/Controllers/IdentityServerLoggerController.cs new file mode 100644 index 0000000..c813ef5 --- /dev/null +++ b/RobotNet.IdentityServer/Controllers/IdentityServerLoggerController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RobotNet.IdentityServer.Controllers; + +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class IdentityServerLoggerController(ILogger Logger) : ControllerBase +{ + private readonly string LoggerDirectory = "identityServerlogs"; + + [HttpGet] + public async Task> GetLogs([FromQuery(Name = "date")] DateTime date) + { + string temp = ""; + try + { + string fileName = $"{date:yyyy-MM-dd}.log"; + string path = Path.Combine(LoggerDirectory, fileName); + if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory))) + { + Logger.LogWarning($"GetLogs: phát hiện đường dẫn không hợp lệ."); + return []; + } + + if (!System.IO.File.Exists(path)) + { + Logger.LogWarning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}."); + return []; + } + + temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log"); + System.IO.File.Copy(path, temp); + + return await System.IO.File.ReadAllLinesAsync(temp); + } + catch (Exception ex) + { + Logger.LogWarning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + finally + { + if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp); + } + } +} diff --git a/RobotNet.IdentityServer/Controllers/UserinfoController.cs b/RobotNet.IdentityServer/Controllers/UserinfoController.cs new file mode 100644 index 0000000..d61439b --- /dev/null +++ b/RobotNet.IdentityServer/Controllers/UserinfoController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using RobotNet.IdentityServer.Data; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace RobotNet.IdentityServer.Controllers; + +[EnableCors("RequestAuthorize")] +[Route("api/[controller]")] +[ApiController] +public class UserinfoController(UserManager userManager) : ControllerBase +{// GET: /api/userinfo + [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] + [HttpGet(""), HttpPost(""), Produces("application/json")] + public async Task Userinfo() + { + var user = await userManager.FindByIdAsync(User.GetClaim(Claims.Subject) ?? ""); + if (user == null) + { + return Challenge( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidToken, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = + "The specified access token is bound to an account that no longer exists." + })); + } + + var claims = new Dictionary(StringComparer.Ordinal) + { + // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. + [Claims.Subject] = await userManager.GetUserIdAsync(user) + }; + + if (User.HasScope(Scopes.Email)) + { + claims[Claims.Email] = await userManager.GetEmailAsync(user) ?? ""; + claims[Claims.EmailVerified] = await userManager.IsEmailConfirmedAsync(user); + } + + if (User.HasScope(Scopes.Phone)) + { + claims[Claims.PhoneNumber] = await userManager.GetPhoneNumberAsync(user) ?? ""; + claims[Claims.PhoneNumberVerified] = await userManager.IsPhoneNumberConfirmedAsync(user); + } + + if (User.HasScope(Scopes.Roles)) + { + claims[Claims.Role] = await userManager.GetRolesAsync(user); + } + + // Note: the complete list of standard claims supported by the OpenID Connect specification + // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + + return Ok(claims); + } +} diff --git a/RobotNet.IdentityServer/Data/ApplicationDbContext.cs b/RobotNet.IdentityServer/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..120095b --- /dev/null +++ b/RobotNet.IdentityServer/Data/ApplicationDbContext.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.IdentityServer.Data +{ + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext(DbContextOptions options) : base(options) + { + } + + } +} diff --git a/RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs b/RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs new file mode 100644 index 0000000..4b6dfcd --- /dev/null +++ b/RobotNet.IdentityServer/Data/ApplicationDbExtensions.cs @@ -0,0 +1,190 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace RobotNet.IdentityServer.Data; + +public static class ApplicationDbExtensions +{ + public static async Task SeedApplicationDbAsync(this IServiceProvider serviceProvider) + { + using var scope = serviceProvider.GetRequiredService().CreateScope(); + + using var appDb = scope.ServiceProvider.GetRequiredService(); + + await appDb.Database.MigrateAsync(); + //await appDb.Database.EnsureCreatedAsync(); + await appDb.SaveChangesAsync(); + + await scope.ServiceProvider.SeedRolesAsync(); + await scope.ServiceProvider.SeedUsersAsync(); + await scope.ServiceProvider.SeedOpenIddictApplicationAsync(); + await scope.ServiceProvider.SeedOpenIddictScopesAsync(); + } + + private static async Task SeedRolesAsync(this IServiceProvider serviceProvider) + { + var roleManager = serviceProvider.GetRequiredService>(); + if (!await roleManager.RoleExistsAsync("Administrator")) + { + await roleManager.CreateAsync(new ApplicationRole() + { + Name = "Administrator", + NormalizedName = "ADMINISTRATOR", + CreatedDate = DateTime.UtcNow + }); + } + } + + private static async Task SeedUsersAsync(this IServiceProvider serviceProvider) + { + using var userManager = serviceProvider.GetRequiredService>(); + if (await userManager.FindByNameAsync("admin") is null) + { + var admin = new ApplicationUser() + { + UserName = "admin", + Email = "administrator@phenikaa-x.com", + NormalizedUserName = "ADMINISTRATOR", + NormalizedEmail = "ADMINISTRATOR@PHENIKAA-X.COM", + EmailConfirmed = true, + }; + + await userManager.CreateAsync(admin, "robotics"); + await userManager.AddToRoleAsync(admin, "Administrator"); + } + } + + private static async Task CreateIfNotExistAsync(this IOpenIddictApplicationManager manager, OpenIddictApplicationDescriptor desciptor) + { + if (desciptor.ClientId == null) return; + if (await manager.FindByClientIdAsync(desciptor.ClientId) == null) + { + await manager.CreateAsync(desciptor); + } + } + + private static async Task SeedOpenIddictApplicationAsync(this IServiceProvider serviceProvider) + { + var manager = serviceProvider.GetRequiredService(); + + await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor + { + ClientId = "robotnet-webapp", + ConsentType = ConsentTypes.Explicit, + DisplayName = "RobotNet WebApp", + ClientType = ClientTypes.Public, + PostLogoutRedirectUris = + { + new Uri("https://localhost:7035/authentication/logout-callback") + }, + RedirectUris = + { + new Uri("https://localhost:7035/authentication/login-callback") + }, + Permissions = + { + Permissions.Endpoints.Authorization, + Permissions.Endpoints.EndSession, + Permissions.Endpoints.Token, + Permissions.GrantTypes.AuthorizationCode, + Permissions.GrantTypes.RefreshToken, + Permissions.ResponseTypes.Code, + Permissions.Scopes.Email, + Permissions.Scopes.Profile, + Permissions.Scopes.Roles, + Permissions.Prefixes.Scope + "robotnet-script-api", + Permissions.Prefixes.Scope + "robotnet-robot-api", + Permissions.Prefixes.Scope + "robotnet-map-api", + }, + Requirements = + { + Requirements.Features.ProofKeyForCodeExchange, + }, + }); + + + await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor + { + ClientId = "robotnet-script-manager", + ClientSecret = "05594ECB-BBAE-4246-8EED-4F0841C3B475", + Permissions = + { + Permissions.Endpoints.Introspection, + Permissions.GrantTypes.ClientCredentials, + Permissions.Endpoints.Token, + Permissions.Prefixes.Scope + "robotnet-robot-api", + Permissions.Prefixes.Scope + "robotnet-map-api", + } + }); + + await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor + { + ClientId = "robotnet-map-manager", + ClientSecret = "72B36E68-2F2B-455B-858A-77B1DCC79979", + Permissions = + { + Permissions.Endpoints.Introspection, + } + }); + + await manager.CreateIfNotExistAsync(new OpenIddictApplicationDescriptor + { + ClientId = "robotnet-robot-manager", + ClientSecret = "469B2DEB-660E-4C91-97C7-D69550D9969D", + Permissions = + { + Permissions.Endpoints.Introspection, + Permissions.GrantTypes.ClientCredentials, + Permissions.Endpoints.Token, + Permissions.Prefixes.Scope + "robotnet-map-api", + } + }); + } + + private static async Task CreateIfNotExistAsync(this IOpenIddictScopeManager manager, OpenIddictScopeDescriptor desciptor) + { + if (desciptor.Name == null) return; + if (await manager.FindByNameAsync(desciptor.Name) is null) + { + await manager.CreateAsync(desciptor); + } + } + + private static async Task SeedOpenIddictScopesAsync(this IServiceProvider serviceProvider) + { + var manager = serviceProvider.GetRequiredService(); + + await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor + { + DisplayName = "RobotNet Script Manager API Access", + Name = "robotnet-script-api", + Resources = + { + "robotnet-script-manager" + } + }); + + await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor + { + DisplayName = "RobotNet Map Manager API Access", + Name = "robotnet-map-api", + Resources = + { + "robotnet-map-manager" + } + }); + + await manager.CreateIfNotExistAsync(new OpenIddictScopeDescriptor + { + DisplayName = "RobotNet Robot Manager API Access", + Name = "robotnet-robot-api", + Resources = + { + "robotnet-robot-manager" + } + }); + } + +} diff --git a/RobotNet.IdentityServer/Data/ApplicationRole.cs b/RobotNet.IdentityServer/Data/ApplicationRole.cs new file mode 100644 index 0000000..631a83f --- /dev/null +++ b/RobotNet.IdentityServer/Data/ApplicationRole.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace RobotNet.IdentityServer.Data +{ + public class ApplicationRole : IdentityRole + { + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + } +} diff --git a/RobotNet.IdentityServer/Data/ApplicationUser.cs b/RobotNet.IdentityServer/Data/ApplicationUser.cs new file mode 100644 index 0000000..e68c06a --- /dev/null +++ b/RobotNet.IdentityServer/Data/ApplicationUser.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity; + +namespace RobotNet.IdentityServer.Data +{ + // Add profile data for application users by adding properties to the ApplicationUser class + public class ApplicationUser : IdentityUser + { + public string FullName { get; set; } = ""; + public byte[]? AvatarImage { get; set; } + public string AvatarContentType { get; set; } = ""; + } + +} diff --git a/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs b/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs new file mode 100644 index 0000000..3cea989 --- /dev/null +++ b/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.Designer.cs @@ -0,0 +1,540 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.IdentityServer.Data; + +#nullable disable + +namespace RobotNet.IdentityServer.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250716085859_InitializeApplicationDb")] + partial class InitializeApplicationDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarImage") + .HasColumnType("varbinary(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.cs b/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.cs new file mode 100644 index 0000000..ae5ab5d --- /dev/null +++ b/RobotNet.IdentityServer/Data/Migrations/20250716085859_InitializeApplicationDb.cs @@ -0,0 +1,378 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.IdentityServer.Data.Migrations +{ + /// + public partial class InitializeApplicationDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + CreatedDate = table.Column(type: "datetime2", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + FullName = table.Column(type: "nvarchar(max)", nullable: false), + AvatarImage = table.Column(type: "varbinary(max)", nullable: true), + AvatarContentType = table.Column(type: "nvarchar(max)", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictApplications", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + ApplicationType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + ClientId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + ClientSecret = table.Column(type: "nvarchar(max)", nullable: true), + ClientType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + ConcurrencyToken = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + ConsentType = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + DisplayName = table.Column(type: "nvarchar(max)", nullable: true), + DisplayNames = table.Column(type: "nvarchar(max)", nullable: true), + JsonWebKeySet = table.Column(type: "nvarchar(max)", nullable: true), + Permissions = table.Column(type: "nvarchar(max)", nullable: true), + PostLogoutRedirectUris = table.Column(type: "nvarchar(max)", nullable: true), + Properties = table.Column(type: "nvarchar(max)", nullable: true), + RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), + Requirements = table.Column(type: "nvarchar(max)", nullable: true), + Settings = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictScopes", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + ConcurrencyToken = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Description = table.Column(type: "nvarchar(max)", nullable: true), + Descriptions = table.Column(type: "nvarchar(max)", nullable: true), + DisplayName = table.Column(type: "nvarchar(max)", nullable: true), + DisplayNames = table.Column(type: "nvarchar(max)", nullable: true), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Properties = table.Column(type: "nvarchar(max)", nullable: true), + Resources = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictAuthorizations", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + ApplicationId = table.Column(type: "nvarchar(450)", nullable: true), + ConcurrencyToken = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "datetime2", nullable: true), + Properties = table.Column(type: "nvarchar(max)", nullable: true), + Scopes = table.Column(type: "nvarchar(max)", nullable: true), + Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictAuthorizations_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "OpenIddictTokens", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + ApplicationId = table.Column(type: "nvarchar(450)", nullable: true), + AuthorizationId = table.Column(type: "nvarchar(450)", nullable: true), + ConcurrencyToken = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + CreationDate = table.Column(type: "datetime2", nullable: true), + ExpirationDate = table.Column(type: "datetime2", nullable: true), + Payload = table.Column(type: "nvarchar(max)", nullable: true), + Properties = table.Column(type: "nvarchar(max)", nullable: true), + RedemptionDate = table.Column(type: "datetime2", nullable: true), + ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), + Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "OpenIddictApplications", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", + column: x => x.AuthorizationId, + principalTable: "OpenIddictAuthorizations", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_ClientId", + table: "OpenIddictApplications", + column: "ClientId", + unique: true, + filter: "[ClientId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_ApplicationId_Status_Subject_Type", + table: "OpenIddictAuthorizations", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_Name", + table: "OpenIddictScopes", + column: "Name", + unique: true, + filter: "[Name] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ApplicationId_Status_Subject_Type", + table: "OpenIddictTokens", + columns: new[] { "ApplicationId", "Status", "Subject", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_AuthorizationId", + table: "OpenIddictTokens", + column: "AuthorizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_ReferenceId", + table: "OpenIddictTokens", + column: "ReferenceId", + unique: true, + filter: "[ReferenceId] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "OpenIddictScopes"); + + migrationBuilder.DropTable( + name: "OpenIddictTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "OpenIddictAuthorizations"); + + migrationBuilder.DropTable( + name: "OpenIddictApplications"); + } + } +} diff --git a/RobotNet.IdentityServer/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/RobotNet.IdentityServer/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..bf65f43 --- /dev/null +++ b/RobotNet.IdentityServer/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,537 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.IdentityServer.Data; + +#nullable disable + +namespace RobotNet.IdentityServer.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("ClientSecret") + .HasColumnType("nvarchar(max)"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("JsonWebKeySet") + .HasColumnType("nvarchar(max)"); + + b.Property("Permissions") + .HasColumnType("nvarchar(max)"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedirectUris") + .HasColumnType("nvarchar(max)"); + + b.Property("Requirements") + .HasColumnType("nvarchar(max)"); + + b.Property("Settings") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique() + .HasFilter("[ClientId] IS NOT NULL"); + + b.ToTable("OpenIddictApplications", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Scopes") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Descriptions") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("DisplayNames") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("Resources") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[Name] IS NOT NULL"); + + b.ToTable("OpenIddictScopes", (string)null); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ApplicationId") + .HasColumnType("nvarchar(450)"); + + b.Property("AuthorizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("CreationDate") + .HasColumnType("datetime2"); + + b.Property("ExpirationDate") + .HasColumnType("datetime2"); + + b.Property("Payload") + .HasColumnType("nvarchar(max)"); + + b.Property("Properties") + .HasColumnType("nvarchar(max)"); + + b.Property("RedemptionDate") + .HasColumnType("datetime2"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("nvarchar(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique() + .HasFilter("[ReferenceId] IS NOT NULL"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", (string)null); + }); + + modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("RobotNet.IdentityServer.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("AvatarContentType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("AvatarImage") + .HasColumnType("varbinary(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("RobotNet.IdentityServer.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.IdentityServer/Dockerfile b/RobotNet.IdentityServer/Dockerfile new file mode 100644 index 0000000..a34751d --- /dev/null +++ b/RobotNet.IdentityServer/Dockerfile @@ -0,0 +1,56 @@ +FROM alpine:3.22 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["RobotNet.IdentityServer/RobotNet.IdentityServer.csproj", "RobotNet.IdentityServer/"] +COPY ["RobotNet.IdentityServer/libman.json", "RobotNet.IdentityServer/"] +COPY ["RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj", "RobotNet.ServiceDefaults/"] + +# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj" +RUN dotnet restore "RobotNet.IdentityServer/RobotNet.IdentityServer.csproj" + +WORKDIR /src/RobotNet.IdentityServer +RUN dotnet tool install -g Microsoft.Web.LibraryManager.Cli +ENV PATH="${PATH}:/root/.dotnet/tools" +# RUN libman restore + +WORKDIR /src +COPY RobotNet.IdentityServer/ RobotNet.IdentityServer/ +COPY RobotNet.ServiceDefaults/ RobotNet.ServiceDefaults/ + +RUN rm -rf ./RobotNet.IdentityServer/bin +RUN rm -rf ./RobotNet.IdentityServer/obj +RUN rm -rf ./RobotNet.ServiceDefaults/bin +RUN rm -rf ./RobotNet.ServiceDefaults/obj + +WORKDIR "/src/RobotNet.IdentityServer" +RUN dotnet build -c Release -o /app/build + +FROM build AS publish +WORKDIR /src/RobotNet.IdentityServer +RUN dotnet publish "RobotNet.IdentityServer.csproj" \ + -c Release \ + -o /app/publish \ + --runtime linux-musl-x64 \ + --self-contained true \ + /p:PublishTrimmed=false \ + /p:PublishReadyToRun=true + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish ./ + +RUN apk add --no-cache icu-libs tzdata ca-certificates + +RUN echo '#!/bin/sh' >> ./start.sh +RUN echo 'update-ca-certificates' >> ./start.sh +RUN echo 'exec ./RobotNet.IdentityServer' >> ./start.sh + +RUN chmod +x ./RobotNet.IdentityServer +RUN chmod +x ./start.sh + +# Use the start script to ensure certificates are updated before starting the application +EXPOSE 443 +ENTRYPOINT ["./start.sh"] \ No newline at end of file diff --git a/RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs b/RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs new file mode 100644 index 0000000..32cd8a5 --- /dev/null +++ b/RobotNet.IdentityServer/Helpers/AsyncEnumerableExtensions.cs @@ -0,0 +1,21 @@ +namespace RobotNet.IdentityServer.Helpers; + +public static class AsyncEnumerableExtensions +{ + public static Task> ToListAsync(this IAsyncEnumerable source) + { + return source == null ? throw new ArgumentNullException(nameof(source)) : ExecuteAsync(); + + async Task> ExecuteAsync() + { + var list = new List(); + + await foreach (var element in source) + { + list.Add(element); + } + + return list; + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Helpers/FormValueRequiredAttribute.cs b/RobotNet.IdentityServer/Helpers/FormValueRequiredAttribute.cs new file mode 100644 index 0000000..010f6e8 --- /dev/null +++ b/RobotNet.IdentityServer/Helpers/FormValueRequiredAttribute.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ActionConstraints; + +namespace RobotNet.IdentityServer.Helpers; + +public sealed class FormValueRequiredAttribute(string name) : ActionMethodSelectorAttribute +{ + private readonly string _name = name; + + public override bool IsValidForRequest(RouteContext context, ActionDescriptor action) + { + if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) || + string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType)) + { + return false; + } + + if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]); + } +} diff --git a/RobotNet.IdentityServer/Program.cs b/RobotNet.IdentityServer/Program.cs new file mode 100644 index 0000000..fe625e8 --- /dev/null +++ b/RobotNet.IdentityServer/Program.cs @@ -0,0 +1,196 @@ +using BlazorComponentBus; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using MudBlazor.Services; +using NLog.Web; +using Quartz; +using RobotNet.IdentityServer.Components; +using RobotNet.IdentityServer.Components.Account; +using RobotNet.IdentityServer.Components.Layout; +using RobotNet.IdentityServer.Data; +using RobotNet.IdentityServer.Services; +using System.Security.Cryptography.X509Certificates; +using static OpenIddict.Abstractions.OpenIddictConstants; + +var builder = WebApplication.CreateBuilder(args); + +builder.Host.UseNLog(); +// builder.AddServiceDefaults(); + +builder.Services.AddControllers(); +builder.Services.AddControllersWithViews(); +builder.Services.AddMudServices(); +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +builder.Services.AddScoped(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(connectionString); + options.UseOpenIddict(); +}); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = true; + options.Lockout.AllowedForNewUsers = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + options.Password.RequireDigit = false; + }) + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + +builder.Services.AddSingleton, IdentityNoOpEmailSender>(); + + +builder.Services.AddQuartz(options => +{ + options.UseSimpleTypeLoader(); + options.UseInMemoryStore(); +}); + +builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + +builder.Services.AddOpenIddict() + .AddCore(options => + { + // Configure OpenIddict to use the Entity Framework Core stores and models. + // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities. + options.UseEntityFrameworkCore() + .UseDbContext(); + + // Enable Quartz.NET integration. + options.UseQuartz(); + }) + .AddServer(options => + { + options.SetIssuer(builder.Configuration["OpenIddictCertificate:Issuer"] ?? throw new InvalidOperationException("OpenIddictCertificate Issuer is not configured.")); + + // Enable the authorization, logout, token and userinfo endpoints. + options.SetAuthorizationEndpointUris("api/Authorization/connect/authorize") + .SetEndSessionEndpointUris("api/Authorization/connect/logout") + .SetIntrospectionEndpointUris("connect/introspect") + .SetTokenEndpointUris("api/Authorization/connect/token") + .AllowClientCredentialsFlow() + .SetUserInfoEndpointUris("api/Userinfo") + .SetEndUserVerificationEndpointUris("connect/verify"); + + // Mark the "email", "profile" and "roles" scopes as supported scopes. + options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles); + + // Note: this sample only uses the authorization code and refresh token + // flows but you can enable the other flows if you need to support + // implicit, password or client credentials. + options.AllowAuthorizationCodeFlow() + .AllowRefreshTokenFlow() + .AllowClientCredentialsFlow(); + + if (builder.Environment.IsDevelopment()) + { + // Register the signing and encryption credentials. + options.AddDevelopmentEncryptionCertificate() + .AddDevelopmentSigningCertificate(); + + // Thêm ephemeral encryption key + //options.AddEphemeralEncryptionKey() + // .AddEphemeralSigningKey(); // Thêm signing key tạm thời + } + else if (builder.Environment.IsProduction()) + { + // Thêm ephemeral encryption key + // Sử dụng chứng chỉ thực tế + var path = builder.Configuration["OpenIddictCertificate:Path"] ?? throw new InvalidOperationException("Certificate path is not configured."); + var password = builder.Configuration["OpenIddictCertificate:Password"] ?? throw new InvalidOperationException("Certificate password is not configured."); + if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(password)) + { + throw new InvalidOperationException("Certificate path or password is not configured."); + } + + var certificate = X509CertificateLoader.LoadPkcs12FromFile(path, password); + options.AddEncryptionCertificate(certificate) + .AddSigningCertificate(certificate); + } + + options.UseDataProtection() + .PreferDefaultAccessTokenFormat() + .PreferDefaultAuthorizationCodeFormat() + .PreferDefaultRefreshTokenFormat(); + // Register the ASP.NET Core host and configure the ASP.NET Core-specific options. + options.UseAspNetCore() + .EnableAuthorizationEndpointPassthrough() + .EnableEndSessionEndpointPassthrough() + .EnableTokenEndpointPassthrough() + .EnableUserInfoEndpointPassthrough() + .EnableStatusCodePagesIntegration(); + // Can thiệp vào sự kiện logging + }) + .AddValidation(options => + { + // Import the configuration from the local OpenIddict server instance. + options.UseLocalServer(); + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }); + +builder.Services.AddCors(options => +{ + options.AddPolicy("RequestAuthorize", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +builder.Services.AddMudServices(); + +var app = builder.Build(); + +await app.Services.SeedApplicationDbAsync(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseCors("RequestAuthorize"); + +app.MapControllers(); +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.MapAdditionalIdentityEndpoints(); + +app.Run(); diff --git a/RobotNet.IdentityServer/Properties/launchSettings.json b/RobotNet.IdentityServer/Properties/launchSettings.json new file mode 100644 index 0000000..f87e9dc --- /dev/null +++ b/RobotNet.IdentityServer/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "workingDirectory": "$(TargetDir)", + "applicationUrl": "https://localhost:7061", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/RobotNet.IdentityServer/Properties/serviceDependencies.json b/RobotNet.IdentityServer/Properties/serviceDependencies.json new file mode 100644 index 0000000..d8177e0 --- /dev/null +++ b/RobotNet.IdentityServer/Properties/serviceDependencies.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql", + "connectionId": "ConnectionStrings:DefaultConnection" + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/Properties/serviceDependencies.local.json b/RobotNet.IdentityServer/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..299aa9a --- /dev/null +++ b/RobotNet.IdentityServer/Properties/serviceDependencies.local.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql.local", + "connectionId": "ConnectionStrings:DefaultConnection" + } + } +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/RobotNet.IdentityServer.csproj b/RobotNet.IdentityServer/RobotNet.IdentityServer.csproj new file mode 100644 index 0000000..87917cb --- /dev/null +++ b/RobotNet.IdentityServer/RobotNet.IdentityServer.csproj @@ -0,0 +1,41 @@ + + + + net9.0 + enable + enable + aspnet-RobotNet.IdentityServer-e398adbb-379f-421d-8396-f36f060aca5f + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + PreserveNewest + true + PreserveNewest + + + + diff --git a/RobotNet.IdentityServer/Services/IdentityService.cs b/RobotNet.IdentityServer/Services/IdentityService.cs new file mode 100644 index 0000000..70f67eb --- /dev/null +++ b/RobotNet.IdentityServer/Services/IdentityService.cs @@ -0,0 +1,69 @@ +// IdentityService.cs - Tạo dịch vụ này để tránh lỗi DbContext + +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using RobotNet.IdentityServer.Data; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace RobotNet.IdentityServer.Services; +public class IdentityService +{ + private readonly IServiceProvider _serviceProvider; + + public IdentityService(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task GetUserByIdAsync(string userId) + { + using var scope = _serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + var user = await userManager.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Id == userId); + return user; + } + + public async Task GetUserByNameAsync(string userName) + { + using var scope = _serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await userManager.FindByNameAsync(userName); + } + + public async Task> GetUserRolesAsync(ApplicationUser user) + { + using var scope = _serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roles = await userManager.GetRolesAsync(user); + return roles.ToList(); + } + + public async Task UpdateUserAsync(ApplicationUser user) + { + using var scope = _serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + + var existingUser = await userManager.FindByIdAsync(user.Id); + if (existingUser != null) + { + var context = scope.ServiceProvider.GetRequiredService(); + context.Entry(existingUser).State = EntityState.Detached; + } + + return await userManager.UpdateAsync(user); + } + + public async Task ChangePasswordAsync(ApplicationUser user, string currentPassword, string newPassword) + { + using var scope = _serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + return await userManager.ChangePasswordAsync(user, currentPassword, newPassword); + } +} + + diff --git a/RobotNet.IdentityServer/Services/PasswordStrengthService.cs b/RobotNet.IdentityServer/Services/PasswordStrengthService.cs new file mode 100644 index 0000000..53a60a4 --- /dev/null +++ b/RobotNet.IdentityServer/Services/PasswordStrengthService.cs @@ -0,0 +1,61 @@ +using MudBlazor; + +namespace RobotNet.IdentityServer.Services; + +public class PasswordStrengthService +{ + /// + /// Đánh giá độ mạnh của mật khẩu (thang điểm 0-100) + /// + /// Mật khẩu cần đánh giá + /// Điểm đánh giá từ 0-100 + public int EvaluatePasswordStrength(string password) + { + if (string.IsNullOrEmpty(password)) + return 0; + + int strength = 0; + + // Đánh giá dựa trên độ dài + if (password.Length >= 1) strength += 5; + if (password.Length >= 3) strength += 5; + if (password.Length >= 6) strength += 10; + if (password.Length >= 8) strength += 10; + if (password.Length >= 10) strength += 10; + + // Đánh giá dựa trên độ phức tạp + if (password.Any(char.IsUpper)) strength += 15; + if (password.Any(char.IsLower)) strength += 15; + if (password.Any(char.IsDigit)) strength += 15; + if (password.Any(c => !char.IsLetterOrDigit(c))) strength += 15; + + return System.Math.Min(strength, 100); + } + + /// + /// Lấy màu tương ứng với độ mạnh của mật khẩu + /// + /// Điểm đánh giá độ mạnh (0-100) + /// Color tương ứng + public Color GetStrengthColor(int strength) + { + if (strength < 30) return Color.Error; + if (strength < 60) return Color.Warning; + if (strength < 80) return Color.Info; + return Color.Success; + } + + /// + /// Lấy mô tả tương ứng với độ mạnh của mật khẩu + /// + /// Điểm đánh giá độ mạnh (0-100) + /// Mô tả dạng văn bản + public string GetStrengthDescription(int strength) + { + if (strength == 0) return "Chưa nhập mật khẩu"; + if (strength < 30) return "Mật khẩu yếu"; + if (strength < 60) return "Mật khẩu trung bình"; + if (strength < 80) return "Mật khẩu tốt"; + return "Mật khẩu mạnh"; + } +} diff --git a/RobotNet.IdentityServer/Services/UserImageService.cs b/RobotNet.IdentityServer/Services/UserImageService.cs new file mode 100644 index 0000000..34cb2a9 --- /dev/null +++ b/RobotNet.IdentityServer/Services/UserImageService.cs @@ -0,0 +1,26 @@ +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Processing; + + +namespace RobotNet.IdentityServer.Services; + +public class UserImageService +{ + public async Task<(byte[] ImageBytes, string ContentType)> ResizeAndConvertAsync(Stream input) + { + using var image = await Image.LoadAsync(input); + image.Mutate(x => x.Resize(new ResizeOptions + { + Size = new Size(300, 300), + Mode = ResizeMode.Crop + })); + + using var ms = new MemoryStream(); + await image.SaveAsJpegAsync(ms, new JpegEncoder { Quality = 90 }); + + return (ms.ToArray(), "image/jpeg"); + } +} diff --git a/RobotNet.IdentityServer/Services/UserInfoService.cs b/RobotNet.IdentityServer/Services/UserInfoService.cs new file mode 100644 index 0000000..92a1ce6 --- /dev/null +++ b/RobotNet.IdentityServer/Services/UserInfoService.cs @@ -0,0 +1,41 @@ +namespace RobotNet.IdentityServer.Services; + +public class UserInfoService +{ + + private readonly List> _handlers = []; + + + public void RegisterHandler(Func handler) + { + if (handler != null && !_handlers.Contains(handler)) + { + _handlers.Add(handler); + } + } + public void UnregisterHandler(Func handler) + { + if (handler != null && _handlers.Contains(handler)) + { + _handlers.Remove(handler); + } + } + + + public async Task NotifyUserInfoChanged() + { + var handlers = new List>(_handlers); + + foreach (var handler in handlers) + { + try + { + await handler(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in user info change handler: {ex.Message}"); + } + } + } +} diff --git a/RobotNet.IdentityServer/appsettings.json b/RobotNet.IdentityServer/appsettings.json new file mode 100644 index 0000000..a1957bf --- /dev/null +++ b/RobotNet.IdentityServer/appsettings.json @@ -0,0 +1,18 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=172.20.235.170;Database=RobotNet.Identity;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Warning" + } + }, + "AllowedHosts": "*", + "OpenIddictCertificate": { + "Issuer": "https://localhost:7061", + "Path": "/app/certs/robotnet.pfx", + "Password": "RobotNet@2024" + } +} diff --git a/RobotNet.IdentityServer/libman.json b/RobotNet.IdentityServer/libman.json new file mode 100644 index 0000000..066bc2d --- /dev/null +++ b/RobotNet.IdentityServer/libman.json @@ -0,0 +1,15 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.3", + "destination": "wwwroot/lib/bootstrap/" + }, + { + "provider": "jsdelivr", + "library": "@mdi/font@7.4.47", + "destination": "wwwroot/lib/mdi/font/" + } + ] +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/nlog.config b/RobotNet.IdentityServer/nlog.config new file mode 100644 index 0000000..68b8d0f --- /dev/null +++ b/RobotNet.IdentityServer/nlog.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.IdentityServer/wwwroot/app.css b/RobotNet.IdentityServer/wwwroot/app.css new file mode 100644 index 0000000..73a69d6 --- /dev/null +++ b/RobotNet.IdentityServer/wwwroot/app.css @@ -0,0 +1,60 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} \ No newline at end of file diff --git a/RobotNet.IdentityServer/wwwroot/favicon.svg b/RobotNet.IdentityServer/wwwroot/favicon.svg new file mode 100644 index 0000000..b07dbc4 --- /dev/null +++ b/RobotNet.IdentityServer/wwwroot/favicon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 b/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b0ed6d697b45af5d168e2095a7f9d42ab5037983 GIT binary patch literal 11840 zcmV-GF2B)tPew8T0RR9104_iP5&!@I09*6`04>@80RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 b/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..de646f8d4fbfcc314053710c299fcc60716999b0 GIT binary patch literal 9644 zcmV;dB~#jWPew8T0RR91041yd5&!@I07Mi303}BN0RR9100000000000000000000 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 Pab&0C?+heW+1Nz@tgQ^J literal 0 HcmV?d00001 diff --git a/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 b/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..47e69cf8aa8ea6c5daeff3acd53343d5519602d3 GIT binary patch literal 13740 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 W$zFi)LqQISL(Z9if!eGx0RRC0y_JFh literal 0 HcmV?d00001 diff --git a/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 b/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bc95855cdb4d03ee9a9999e8a4152f94dfcaf123 GIT binary patch literal 7856 zcmV;h9#7$SPew8T0RR9103NUa5&!@I073u&03JyI0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 b/RobotNet.IdentityServer/wwwroot/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..15e1583a0f7320719a1e47697fb1986aea0a829f GIT binary patch literal 19660 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⪻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

KnGS{QdFkQ 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 z�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 O Logger) : ControllerBase +{ + [HttpPost] + public async Task> CreateAction([FromBody] ActionCreateModel model) + { + try + { + var map = await MapDb.Maps.FindAsync(model.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}"); + + if (MapDb.Actions.Any(action => action.Name == model.Name && action.MapId == model.MapId)) return new(false, $"Tên Action {model.Name} đã tồn tại"); + + var entity = await MapDb.Actions.AddAsync(new() + { + MapId = model.MapId, + Name = model.Name, + Content = model.Content, + }); + await MapDb.SaveChangesAsync(); + + return new(true) + { + Data = new() + { + Id = entity.Entity.Id, + MapId = entity.Entity.MapId, + Name = entity.Entity.Name, + Content = entity.Entity.Content, + } + }; + } + catch(Exception ex) + { + Logger.Warning($"CreateAction: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"CreateAction: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpGet] + [Route("{id}")] + public async Task> GetActions(Guid id) + { + return await MapDb.Actions.Where(action => action.MapId == id).Select(action => new ActionDto() + { + Id = action.Id, + MapId = action.MapId, + Name = action.Name, + Content = action.Content, + }).ToListAsync(); + } + + [HttpPut] + public async Task UpdateAction([FromBody] ActionDto model) + { + try + { + var action = await MapDb.Actions.FindAsync(model.Id); + if (action is not null) + { + action.Name = model.Name; + action.Content = model.Content; + MapDb.Actions.Update(action); + await MapDb.SaveChangesAsync(); + return new(true); + } + return new(false, $"Hệ thống không tìm thấy Action {model.Name} này"); + } + catch (Exception ex) + { + Logger.Warning($"UpdateAction: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateAction: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task DeleteAction(Guid id) + { + try + { + var action = await MapDb.Actions.FindAsync(id); + if (action is not null) + { + foreach (var node in MapDb.Nodes) + { + var actionIds = JsonSerializer.Deserialize(node.Actions); + if (actionIds is not null && actionIds.Any(a => a == action.Id)) + { + var acitonIdsAfter = actionIds.ToList(); + acitonIdsAfter.Remove(action.Id); + node.Actions = JsonSerializer.Serialize(acitonIdsAfter.Count > 0 ? acitonIdsAfter : []); + } + } + foreach (var edge in MapDb.Edges) + { + var actionIds = JsonSerializer.Deserialize(edge.Actions); + if (actionIds is not null && actionIds.Any(a => a == action.Id)) + { + var acitonIdsAfter = actionIds.ToList(); + acitonIdsAfter.Remove(action.Id); + edge.Actions = JsonSerializer.Serialize(acitonIdsAfter.Count > 0 ? acitonIdsAfter : []); + } + } + MapDb.Actions.Remove(action); + await MapDb.SaveChangesAsync(); + return new(true) ; + } + return new(false, $"Hệ thống không tìm thấy Action {id} này"); + } + catch (Exception ex) + { + Logger.Warning($"DeleteAction {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"DeleteAction {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/EdgesController.cs b/RobotNet.MapManager/Controllers/EdgesController.cs new file mode 100644 index 0000000..38a6fd3 --- /dev/null +++ b/RobotNet.MapManager/Controllers/EdgesController.cs @@ -0,0 +1,269 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.Shares; +using System.Text.Json; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class EdgesController(MapEditorDbContext MapDb, LoggerController Logger) : ControllerBase +{ + [HttpPost] + [Route("")] + public async Task> CreateEdge([FromBody] EdgeCreateModel model) + { + try + { + var map = await MapDb.Maps.FindAsync(model.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}"); + + if (Math.Sqrt(Math.Pow(model.X1 - model.X2, 2) + Math.Pow(model.Y1 - model.Y2, 2)) < map.EdgeMinLengthDefault) return new(false, "Độ dài edge quá nhỏ"); + + var nodes = await MapDb.Nodes.Where(n => n.MapId == model.MapId).ToListAsync(); + var edges = await MapDb.Edges.Where(e => e.MapId == model.MapId && e.TrajectoryDegree == TrajectoryDegree.One).ToListAsync(); + + var closesStartNode = MapEditorHelper.GetClosesNode(model.X1, model.Y1, [.. nodes.Select(node => new NodeDto() + { + MapId = node.MapId, + X = node.X, + Y = node.Y, + Theta = node.Theta, + Actions = node.Actions, + Id = node.Id, + Name = node.Name, + AllowedDeviationTheta = node.AllowedDeviationTheta, + AllowedDeviationXy = node.AllowedDeviationXy + })]); + + var closesEndNode = MapEditorHelper.GetClosesNode(model.X2, model.Y2, [.. nodes.Select(node => new NodeDto() + { + MapId = node.MapId, + X = node.X, + Y = node.Y, + Theta = node.Theta, + Actions = node.Actions, + Id = node.Id, + Name = node.Name, + AllowedDeviationTheta = node.AllowedDeviationTheta, + AllowedDeviationXy = node.AllowedDeviationXy + })]); + + Node? startNode = await MapDb.Nodes.FindAsync(closesStartNode?.Id); + Node? endNode = await MapDb.Nodes.FindAsync(closesEndNode?.Id); + List RemoveEdge = []; + List AddEdgeDto = []; + + if (startNode is null) + { + startNode = ServerHelper.CreateNode(map, model.X1, model.Y1); + await MapDb.Nodes.AddAsync(startNode); + var closesEdge = ServerHelper.GetClosesEdge(model.X1, model.Y1, nodes, edges, 0.1); + if (closesEdge is not null) + { + var closesEdgeStartNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.StartNodeId); + var closesEdgeEndNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.EndNodeId); + if (closesEdgeStartNode is not null && closesEdgeEndNode is not null) + { + var startEdge = ServerHelper.CreateEdge(map, closesEdgeStartNode.Id, startNode.Id, TrajectoryDegree.One); + var endEdge = ServerHelper.CreateEdge(map, startNode.Id, closesEdgeEndNode.Id, TrajectoryDegree.One); + await MapDb.Edges.AddAsync(startEdge); + await MapDb.Edges.AddAsync(endEdge); + + MapDb.Edges.Remove(closesEdge); + edges.Remove(closesEdge); + RemoveEdge.Add(closesEdge.Id); + + edges.Add(startEdge); + edges.Add(endEdge); + nodes.Add(startNode); + AddEdgeDto.Add(ServerHelper.CreateEdgeDto(startEdge, closesEdgeStartNode, startNode)); + AddEdgeDto.Add(ServerHelper.CreateEdgeDto(endEdge, startNode, closesEdgeEndNode)); + } + } + } + await MapDb.SaveChangesAsync(); + + if (endNode is null) + { + endNode = ServerHelper.CreateNode(map, model.X2, model.Y2); + await MapDb.Nodes.AddAsync(endNode); + + var closesEdge = ServerHelper.GetClosesEdge(model.X2, model.Y2, nodes, edges, 0.1); + if (closesEdge is not null) + { + var closesEdgeStartNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.StartNodeId); + var closesEdgeEndNode = await MapDb.Nodes.FirstOrDefaultAsync(n => n.Id == closesEdge.EndNodeId); + if (closesEdgeStartNode is not null && closesEdgeEndNode is not null) + { + var startEdge = ServerHelper.CreateEdge(map, closesEdgeStartNode.Id, endNode.Id, TrajectoryDegree.One); + var endEdge = ServerHelper.CreateEdge(map, endNode.Id, closesEdgeEndNode.Id, TrajectoryDegree.One); + await MapDb.Edges.AddAsync(startEdge); + await MapDb.Edges.AddAsync(endEdge); + + MapDb.Edges.Remove(closesEdge); + RemoveEdge.Add(closesEdge.Id); + AddEdgeDto.Add(ServerHelper.CreateEdgeDto(startEdge, closesEdgeStartNode, endNode)); + AddEdgeDto.Add(ServerHelper.CreateEdgeDto(endEdge, endNode, closesEdgeEndNode)); + } + } + } + + var edge = ServerHelper.CreateEdge(map, startNode.Id, endNode.Id, model.TrajectoryDegree, model.ControlPoint1X, model.ControlPoint1Y, model.ControlPoint2X, model.ControlPoint2Y); + await MapDb.Edges.AddAsync(edge); + + await MapDb.SaveChangesAsync(); + + AddEdgeDto.Add(ServerHelper.CreateEdgeDto(edge, startNode, endNode)); + + return new(true) + { + Data = new EdgeCreateDto() + { + EdgesDto = AddEdgeDto, + RemoveEdge = RemoveEdge, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"CreateEdge: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"CreateEdge: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task DeleteEdge(Guid id) + { + using var transaction = await MapDb.Database.BeginTransactionAsync(); + try + { + var edge = await MapDb.Edges.FindAsync(id); + if (edge == null) return new(false, $"Không tồn tại edge id = {id}"); + + MapDb.Edges.Remove(edge); + + if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.StartNodeId || e.EndNodeId == edge.StartNodeId) && e.Id != edge.Id)) + { + var node = await MapDb.Nodes.FindAsync(edge.StartNodeId); + if (node != null) + { + var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id); + if (element is not null) MapDb.Elements.Remove(element); + MapDb.Nodes.Remove(node); + } + } + + if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.EndNodeId || e.EndNodeId == edge.EndNodeId) && e.Id != edge.Id)) + { + var node = await MapDb.Nodes.FindAsync(edge.EndNodeId); + if (node != null) + { + var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id); + if (element is not null) MapDb.Elements.Remove(element); + MapDb.Nodes.Remove(node); + } + } + + await MapDb.SaveChangesAsync(); + await transaction.CommitAsync(); + return new(true); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + Logger.Warning($"DeleteEdge {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"DeleteEdge {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("")] + public async Task DeleteEdges([FromBody] IEnumerable DeleteEdgesId) + { + try + { + List deleteEdges = []; + List deleteNodes = []; + foreach (var edgeId in DeleteEdgesId) + { + var edge = await MapDb.Edges.FindAsync(edgeId); + if (edge == null) continue; + + MapDb.Edges.Remove(edge); + + if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.StartNodeId || e.EndNodeId == edge.StartNodeId) && e.Id != edge.Id)) + { + var node = await MapDb.Nodes.FindAsync(edge.StartNodeId); + if (node != null) + { + var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id); + if (element is not null) MapDb.Elements.Remove(element); + MapDb.Nodes.Remove(node); + } + } + + if (!MapDb.Edges.Any(e => (e.StartNodeId == edge.EndNodeId || e.EndNodeId == edge.EndNodeId) && e.Id != edge.Id)) + { + var node = await MapDb.Nodes.FindAsync(edge.EndNodeId); + if (node != null) + { + var element = await MapDb.Elements.FirstOrDefaultAsync(e => e.NodeId == node.Id); + if (element is not null) MapDb.Elements.Remove(element); + MapDb.Nodes.Remove(node); + } + } + + await MapDb.SaveChangesAsync(); + } + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"DeleteEdges: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"DeleteEdges: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + [Route("")] + public async Task UpdateEdge([FromBody] EdgeUpdateModel model) + { + try + { + var edge = await MapDb.Edges.FindAsync(model.Id); + if (edge == null) return new(false, $"Không tồn tại edge id = {model.Id}"); + + edge.MaxSpeed = model.MaxSpeed; + edge.MaxHeight = model.MaxHeight; + edge.MinHeight = model.MinHeight; + edge.ControlPoint1X = model.ControlPoint1X; + edge.ControlPoint1Y = model.ControlPoint1Y; + edge.ControlPoint2X = model.ControlPoint2X; + edge.ControlPoint2Y = model.ControlPoint2Y; + edge.DirectionAllowed = model.DirectionAllowed; + edge.RotationAllowed = model.RotationAllowed; + edge.MaxRotationSpeed = model.MaxRotationSpeed; + edge.Actions = JsonSerializer.Serialize(model.Actions ?? []); + edge.AllowedDeviationXy = model.AllowedDeviationXy; + edge.AllowedDeviationTheta = model.AllowedDeviationTheta; + + await MapDb.SaveChangesAsync(); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateEdge: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateEdge: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/ElementModelsController.cs b/RobotNet.MapManager/Controllers/ElementModelsController.cs new file mode 100644 index 0000000..7ba8ca9 --- /dev/null +++ b/RobotNet.MapManager/Controllers/ElementModelsController.cs @@ -0,0 +1,258 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ElementModelsController(MapEditorDbContext MapDb, MapEditorStorageRepository MapStorage, LoggerController Logger) : ControllerBase +{ + [HttpGet] + [Route("map/{mapId}")] + public async Task>> Gets(Guid mapId) + { + try + { + return new(true) + { + Data = await (from elm in MapDb.ElementModels + where !string.IsNullOrEmpty(elm.Name) && elm.MapId == mapId + select new ElementModelDto() + { + Id = elm.Id, + MapId = elm.MapId, + Name = elm.Name, + Height = elm.Height, + Width = elm.Width, + Image1Height = elm.Image1Height, + Image2Height = elm.Image2Height, + Image1Width = elm.Image1Width, + Image2Width = elm.Image2Width, + Content = elm.Content, + }).ToListAsync() + }; + + } + catch (Exception ex) + { + Logger.Warning($"Gets: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Gets: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpGet] + [Route("{id}")] + public async Task> Get(Guid id) + { + try + { + var elmodel = await MapDb.ElementModels.FindAsync(id); + if (elmodel is null) return new(false, $"Element Model {id} không tồn tại"); + return new(true) + { + Data = new ElementModelDto() + { + Id = elmodel.Id, + Name = elmodel.Name, + MapId = elmodel.MapId, + Height = elmodel.Height, + Width = elmodel.Width, + Content = elmodel.Content, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"Get {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Get {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPost] + public async Task> CreateElementModel([FromForm] ElementModelCreateModel model, [FromForm(Name = "imageOpen")] IFormFile imageOpen, [FromForm(Name = "imageClose")] IFormFile imageClose) + { + try + { + if (model == null || imageOpen == null || imageClose == null) return new(false, "Dữ liệu đầu vào không hợp lệ"); + var map = await MapDb.Maps.FindAsync(model.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}"); + if (MapDb.ElementModels.Any(elm => elm.Name == model.Name && elm.MapId == model.MapId)) return new(false, $"Tên Model {model.Name} đã tồn tại"); + + var image1 = SixLabors.ImageSharp.Image.Load(imageOpen.OpenReadStream()); + var image2 = SixLabors.ImageSharp.Image.Load(imageClose.OpenReadStream()); + var entity = MapDb.ElementModels.Add(new() + { + Name = model.Name, + MapId = model.MapId, + Width = model.Width, + Height = model.Height, + Image1Height = (ushort)image1.Height, + Image1Width = (ushort)image1.Width, + Image2Height = (ushort)image2.Height, + Image2Width = (ushort)image2.Width, + Content = "", + }); + + await MapDb.SaveChangesAsync(); + + var (isSuccess, message) = await MapStorage.UploadAsync("ElementOpenModels", $"{entity.Entity.Id}", imageOpen.OpenReadStream(), imageOpen.Length, imageOpen.ContentType, CancellationToken.None); + if (!isSuccess) + { + MapDb.ElementModels.Remove(entity.Entity); + await MapDb.SaveChangesAsync(); + return new(false, message); + } + (isSuccess, message) = await MapStorage.UploadAsync("ElementCloseModels", $"{entity.Entity.Id}", imageClose.OpenReadStream(), imageClose.Length, imageClose.ContentType, CancellationToken.None); + if (!isSuccess) + { + MapDb.ElementModels.Remove(entity.Entity); + await MapDb.SaveChangesAsync(); + await MapStorage.DeleteAsync("ElementOpenModels", $"{entity.Entity.Id}", CancellationToken.None); + return new(false, message); + } + + return new(true) + { + Data = new() + { + Id = entity.Entity.Id, + Name = entity.Entity.Name, + MapId = entity.Entity.MapId, + Width = entity.Entity.Width, + Height = entity.Entity.Height, + Image1Height = entity.Entity.Image1Height, + Image1Width = entity.Entity.Image1Width, + Image2Height = entity.Entity.Image2Height, + Image2Width = entity.Entity.Image2Width, + Content = entity.Entity.Content + } + }; + } + catch (Exception ex) + { + Logger.Warning($"CreateElementModel: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"CreateElementModel: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + public async Task> Update([FromBody] ElementModelUpdateModel model) + { + try + { + var elModel = await MapDb.ElementModels.FindAsync(model.Id); + if (elModel is null) return new(false, $"Model {model.Id} không tồn tại"); + if (MapDb.ElementModels.Any(elm => elm.Name == model.Name && elm.MapId == elModel.MapId && model.Id != elModel.Id)) return new(false, $"Tên Model {model.Name} đã tồn tại"); + + elModel.Name = model.Name; + elModel.Width = model.Width; + elModel.Height = model.Height; + elModel.Content = model.Content; + await MapDb.SaveChangesAsync(); + return new(true) + { + Data = new() + { + Id = elModel.Id, + Name = elModel.Name, + MapId = elModel.MapId, + Width = elModel.Width, + Height = elModel.Height, + Image1Height = elModel.Image1Height, + Image1Width = elModel.Image1Width, + Image2Height = elModel.Image2Height, + Image2Width = elModel.Image2Width, + Content = elModel.Content + } + }; + } + catch (Exception ex) + { + Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task Delete(Guid id) + { + try + { + var elModel = await MapDb.ElementModels.FindAsync(id); + if (elModel is null) return new(false, $"Model {id} không tồn tại"); + + await MapStorage.DeleteAsync("ElementOpenModels", id.ToString(), CancellationToken.None); + await MapStorage.DeleteAsync("ElementCloseModels", id.ToString(), CancellationToken.None); + MapDb.Elements.RemoveRange(MapDb.Elements.Where(e => e.ModelId == id)); + MapDb.ElementModels.Remove(elModel); + await MapDb.SaveChangesAsync(); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + [Route("openimage/{id}")] + public async Task UpdateOpenImage(Guid id, [FromForm(Name = "image")] IFormFile image) + { + try + { + var elModel = await MapDb.ElementModels.FindAsync(id); + if (elModel is null) return new(false, $"Model {id} không tồn tại"); + + var imageStream = image.OpenReadStream(); + var (isSuccess, message) = await MapStorage.UploadAsync("ElementOpenModels", $"{elModel.Id}", imageStream, image.Length, image.ContentType, CancellationToken.None); + if (!isSuccess) return new(false, message); + + var imageUpdate = SixLabors.ImageSharp.Image.Load(image.OpenReadStream()); + elModel.Image1Width = (ushort)imageUpdate.Width; + elModel.Image1Height = (ushort)imageUpdate.Height; + await MapDb.SaveChangesAsync(); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateOpenImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateOpenImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + [Route("closeimage/{id}")] + public async Task UpdateCloseImage(Guid id, [FromForm(Name = "image")] IFormFile image) + { + try + { + var elModel = await MapDb.ElementModels.FindAsync(id); + if (elModel is null) return new(false, $"Model {id} không tồn tại"); + + var imageStream = image.OpenReadStream(); + var (isSuccess, message) = await MapStorage.UploadAsync("ElementCloseModels", $"{elModel.Id}", imageStream, image.Length, image.ContentType, CancellationToken.None); + if (!isSuccess) return new(false, message); + + var imageUpdate = SixLabors.ImageSharp.Image.Load(image.OpenReadStream()); + elModel.Image2Width = (ushort)imageUpdate.Width; + elModel.Image2Height = (ushort)imageUpdate.Height; + await MapDb.SaveChangesAsync(); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateCloseImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateCloseImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/ElementsController.cs b/RobotNet.MapManager/Controllers/ElementsController.cs new file mode 100644 index 0000000..55f8ff1 --- /dev/null +++ b/RobotNet.MapManager/Controllers/ElementsController.cs @@ -0,0 +1,211 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ElementsController(MapEditorDbContext MapDb, LoggerController Logger) : ControllerBase +{ + + [HttpGet] + public async Task> GetElement([FromQuery] string mapName, [FromQuery] string elementName) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName); + if (map is null) return new(false, $"Không tồn tại map tên = {mapName}"); + + var el = await MapDb.Elements.FirstOrDefaultAsync(e => e.MapId == map.Id && e.Name == elementName); + if (el is null) return new(false, $"Không tồn tại element name = {elementName}"); + + var elNode = await MapDb.Nodes.FindAsync(el.NodeId); + if (elNode is null) return new(false, $"Không tồn tại node id = {el.NodeId}"); + + return new(true) + { + Data = new() + { + Id = el.Id, + Name = el.Name, + MapId = el.MapId, + IsOpen = el.IsOpen, + NodeId = el.NodeId, + OffsetX = el.OffsetX, + OffsetY = el.OffsetY, + ModelId = el.ModelId, + Content = el.Content, + NodeName = elNode.Name, + X = elNode.X, + Y = elNode.Y, + Theta = elNode.Theta + } + }; + } + catch (Exception ex) + { + Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpGet] + [Route("{modelId}")] + public async Task>> GetElementsByModelId([FromRoute] Guid modelId) + { + try + { + var elModel = await MapDb.ElementModels.FindAsync(modelId); + if (elModel is null) return new(false, $"Không tồn tại model id = {modelId}"); + + return new(true) + { + Data = await (from el in MapDb.Elements + where el.ModelId == modelId + select new ElementDto() + { + Id = el.Id, + MapId = el.MapId, + Name = el.Name, + ModelId = el.ModelId, + ModelName = elModel.Name, + IsOpen = el.IsOpen, + NodeId = el.NodeId, + OffsetX = el.OffsetX, + OffsetY = el.OffsetY, + Content = el.Content, + }).ToListAsync() + }; + } + catch (Exception ex) + { + Logger.Warning($"GetElementsByModelId: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"GetElementsByModelId: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPost] + public async Task> Create([FromBody] ElementCreateModel model) + { + try + { + if (model == null || string.IsNullOrEmpty(model.Name)) return new(false, "Dữ liệu không hợp lệ"); + var map = await MapDb.Maps.FindAsync(model.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {model.MapId}"); + + var node = await MapDb.Nodes.FindAsync(model.NodeId); + if (node == null) return new(false, $"Không tồn tại node id = {model.NodeId}"); + + var elModel = await MapDb.ElementModels.FindAsync(model.ModelId); + if (elModel is null) return new(false, $"Không tồn tại element model id = {model.ModelId}"); + + if (MapDb.Elements.Any(el => el.Name == model.Name && el.MapId == model.MapId)) return new(false, $"Tên Element đã tồn tại"); + + if (MapDb.Elements.Any(el => el.NodeId == model.NodeId)) return new(false, $"Node này đã có Element"); + + var entity = await MapDb.Elements.AddAsync(new() + { + Name = model.Name, + MapId = model.MapId, + NodeId = model.NodeId, + ModelId = model.ModelId, + OffsetX = model.OffsetX, + OffsetY = model.OffsetY, + Content = elModel.Content, + }); + + await MapDb.SaveChangesAsync(); + + return new(true) + { + Data = new() + { + Id = entity.Entity.Id, + MapId = entity.Entity.MapId, + NodeId = entity.Entity.NodeId, + ModelName = elModel.Name, + Name = entity.Entity.Name, + ModelId = entity.Entity.ModelId, + OffsetX = entity.Entity.OffsetX, + OffsetY = entity.Entity.OffsetY, + Content = entity.Entity.Content, + IsOpen = entity.Entity.IsOpen, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"Create: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Create: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + public async Task> Update([FromBody] ElementUpdateModel model) + { + try + { + var element = await MapDb.Elements.FindAsync(model.Id); + if (element == null) return new(false, $"Không tồn tại element id = {model.Id}"); + + var map = await MapDb.Maps.FindAsync(element.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {element.MapId}"); + + if (MapDb.Elements.Any(el => el.Name == model.Name && el.MapId == element.MapId && el.Id != model.Id)) return new(false, $"Tên Element đã tồn tại"); + + element.Name = model.Name; + element.OffsetX = model.OffsetX; + element.OffsetY = model.OffsetY; + element.Content = model.Content; + element.IsOpen = model.IsOpen; + await MapDb.SaveChangesAsync(); + + return new(true) + { + Data = new() + { + Id = element.Id, + MapId = element.MapId, + NodeId = element.NodeId, + Name = element.Name, + ModelId = element.ModelId, + OffsetX = element.OffsetX, + OffsetY = element.OffsetY, + Content = element.Content, + IsOpen = element.IsOpen, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task Delete(Guid id) + { + try + { + var element = await MapDb.Elements.FindAsync(id); + if (element == null) return new(false, $"Không tồn tại element id = {id}"); + + MapDb.Elements.Remove(element); + await MapDb.SaveChangesAsync(); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/ImagesController.cs b/RobotNet.MapManager/Controllers/ImagesController.cs new file mode 100644 index 0000000..c78251e --- /dev/null +++ b/RobotNet.MapManager/Controllers/ImagesController.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.MapManager.Services; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class ImagesController(MapEditorStorageRepository StorageRepo, IHttpClientFactory HttpClientFactory, LoggerController Logger) : ControllerBase +{ + [HttpGet] + [Route("map/{id}")] + public async Task GetMapImage(Guid id) + { + try + { + var (usingLocal, url) = StorageRepo.GetUrl("MapImages", $"{id}"); + if (!usingLocal) + { + var http = HttpClientFactory.CreateClient(); + var imageBytes = await http.GetByteArrayAsync(url); + if (imageBytes != null && imageBytes.Length > 0) return File(imageBytes, "image/png"); + else return NotFound("Không thể lấy được ảnh map."); + } + if (System.IO.File.Exists(url)) return File(System.IO.File.ReadAllBytes(url), "image/png"); + else return NotFound(); + } + catch(Exception ex) + { + Logger.Warning($"GetMapImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return NotFound(); + } + } + + [HttpGet] + [Route("elementModel/{id}")] + public async Task GetElementModelImage(Guid id, [FromQuery] bool IsOpen) + { + try + { + var (usingLocal, url) = StorageRepo.GetUrl(IsOpen ? "ElementOpenModels" : "ElementCloseModels", id.ToString()); + if (!usingLocal) + { + var http = HttpClientFactory.CreateClient(); + var imageBytes = await http.GetByteArrayAsync(url); + if (imageBytes != null && imageBytes.Length > 0) + { + return File(imageBytes, "image/png"); + } + else + { + return NotFound("Không thể lấy được ảnh element model."); + } + } + if (System.IO.File.Exists(url)) return File(System.IO.File.ReadAllBytes(url), "image/png"); + else return NotFound(); + } + catch (Exception ex) + { + Logger.Warning($"GetElementModelImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return NotFound(); + } + } +} + diff --git a/RobotNet.MapManager/Controllers/MapDesignerLoggerController.cs b/RobotNet.MapManager/Controllers/MapDesignerLoggerController.cs new file mode 100644 index 0000000..1f010bf --- /dev/null +++ b/RobotNet.MapManager/Controllers/MapDesignerLoggerController.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.MapManager.Services; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class MapDesignerLoggerController(LoggerController Logger) : ControllerBase +{ + private readonly string LoggerDirectory = "mapManagerlogs"; + + [HttpGet] + public async Task> GetLogs([FromQuery(Name = "date")] DateTime date) + { + string temp = ""; + try + { + string fileName = $"{date:yyyy-MM-dd}.log"; + string path = Path.Combine(LoggerDirectory, fileName); + if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory))) + { + Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ."); + return []; + } + + if (!System.IO.File.Exists(path)) + { + Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}."); + return []; + } + + temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log"); + System.IO.File.Copy(path, temp); + return await System.IO.File.ReadAllLinesAsync(temp); + } + catch(Exception ex) + { + Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + finally + { + if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp); + } + } +} diff --git a/RobotNet.MapManager/Controllers/MapExportController.cs b/RobotNet.MapManager/Controllers/MapExportController.cs new file mode 100644 index 0000000..ae586d3 --- /dev/null +++ b/RobotNet.MapManager/Controllers/MapExportController.cs @@ -0,0 +1,249 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class MapExportController(MapEditorDbContext MapDb, MapEditorStorageRepository StorageRepo, IHttpClientFactory HttpClientFactory, LoggerController Logger) : ControllerBase +{ + private readonly byte[] Key = Encoding.UTF8.GetBytes("2512199802031998"); + private readonly byte[] IV = Encoding.UTF8.GetBytes("2512199802031998"); + + [HttpGet] + [Route("encrypt/{Id}")] + public async Task EncryptMap(Guid Id) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == Id); + if (map is null) return NotFound($"Map {Id} không tồn tại"); + + var (usingLocal, url) = StorageRepo.GetUrl("MapImages", $"{Id}"); + byte[] imageData = await GetImageDataAsync(usingLocal, url); + + var elementModels = MapDb.ElementModels.Where(n => n.MapId == Id); + List ElementModelExport = []; + foreach (var elementModel in elementModels) + { + var getImageOpen = StorageRepo.GetUrl("ElementOpenModels", elementModel.Id.ToString()); + byte[] imageElementModelOpenData = await GetImageDataAsync(getImageOpen.usingLocal, getImageOpen.url); + + var getImageClose = StorageRepo.GetUrl("ElementCloseModels", elementModel.Id.ToString()); + byte[] imageElementModelCloseData = await GetImageDataAsync(getImageClose.usingLocal, getImageClose.url); + + ElementModelExport.Add(new ElementModelExportDto() + { + Id = elementModel.Id, + Height = elementModel.Height, + Width = elementModel.Width, + Image1Height = elementModel.Image1Height, + Image2Height = elementModel.Image2Height, + Image1Width = elementModel.Image1Width, + Image2Width = elementModel.Image2Width, + Name = elementModel.Name, + Content = elementModel.Content, + ImageOpenData = imageElementModelOpenData, + ImageCloseData = imageElementModelCloseData, + }); + } + var nodes = MapDb.Nodes.Where(n => n.MapId == Id); + var edges = MapDb.Edges.Where(n => n.MapId == Id); + var zones = MapDb.Zones.Where(n => n.MapId == Id); + var actions = MapDb.Actions.Where(n => n.MapId == Id); + var elements = MapDb.Elements.Where(n => n.MapId == Id); + + var mapDto = new MapExportDto() + { + Id = Id, + Name = map.Name, + Description = map.Description, + Info = new() + { + OriginX = map.OriginX, + OriginY = map.OriginY, + Resolution = map.Resolution, + ViewX = map.ViewX, + ViewY = map.ViewY, + ViewWidth = map.ViewWidth, + ViewHeight = map.ViewHeight, + VDA5050 = map.VDA5050, + }, + Setting = new() + { + NodeNameAutoGenerate = map.NodeNameAutoGenerate, + NodeNameTemplate = map.NodeNameTemplateDefault, + NodeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault, + NodeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault, + + EdgeStraightMaxSpeed = map.EdgeStraightMaxSpeedDefault, + EdgeCurveMaxSpeed = map.EdgeCurveMaxSpeedDefault, + EdgeMaxRotationSpeed = map.EdgeMaxRotationSpeedDefault, + EdgeMinLength = map.EdgeMinLengthDefault, + EdgeMaxHeight = map.EdgeMaxHeightDefault, + EdgeMinHeight = map.EdgeMinHeightDefault, + EdgeRotationAllowed = map.EdgeRotationAllowedDefault, + EdgeDirectionAllowed = map.EdgeDirectionAllowedDefault, + EdgeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault, + EdgeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault, + + ZoneMinSquare = map.ZoneMinSquareDefault, + }, + Data = new() + { + NodeCount = map.NodeCount, + Nodes = [.. nodes.Select(n => new NodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Theta = n.Theta, + AllowedDeviationXy = n.AllowedDeviationXy, + AllowedDeviationTheta = n.AllowedDeviationTheta, + Actions = n.Actions, + })], + Edges = [.. edges.Select(e => new EdgeDto() + { + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + TrajectoryDegree = e.TrajectoryDegree, + EndNodeId = e.EndNodeId, + StartNodeId = e.StartNodeId, + DirectionAllowed = e.DirectionAllowed, + RotationAllowed = e.RotationAllowed, + AllowedDeviationTheta = e.AllowedDeviationTheta, + AllowedDeviationXy = e.AllowedDeviationXy, + MaxHeight = e.MaxHeight, + MinHeight = e.MinHeight, + MaxSpeed = e.MaxSpeed, + MaxRotationSpeed = e.MaxRotationSpeed, + Actions = e.Actions, + })], + Zones = [.. zones.Select(z => new ZoneDto() + { + Type = z.Type, + X1 = z.X1, + Y1 = z.Y1, + X2 = z.X2, + Y2 = z.Y2, + X3 = z.X3, + Y3 = z.Y3, + X4 = z.X4, + Y4 = z.Y4, + })], + Actions = [.. actions.Select(a => new ActionDto() + { + Id = a.Id, + Name = a.Name, + Content = a.Content, + })], + ElementModels = [.. ElementModelExport], + Elements = [.. elements.Select(e => new ElementDto() + { + Name = e.Name, + IsOpen = e.IsOpen, + ModelId = e.ModelId, + NodeId = e.NodeId, + OffsetX = e.OffsetX, + OffsetY = e.OffsetY, + Content = e.Content, + })], + ImageData = imageData, + } + }; + + var jsonData = JsonSerializer.Serialize(mapDto, JsonOptionExtends.Write); + var data = EncryptDataAES(jsonData, Key, IV); + return File(data, "application/octet-stream", $"{map.Name}.map"); + } + catch (Exception ex) + { + Logger.Warning($"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + return NotFound("Hệ thống có lỗi xảy ra"); + } + } + + [HttpPost] + [Route("decrypt")] + public async Task> DecryptMap([FromForm(Name = "importmap")] IFormFile file) + { + try + { + if (file == null || file.Length == 0) return new(false, "File không hợp lệ"); + if (!file.FileName.EndsWith(".map", StringComparison.OrdinalIgnoreCase)) return new(false, "Định dạng file không hợp lệ, yêu cầu file .map"); + using var memoryStream = new MemoryStream(); + await file.CopyToAsync(memoryStream); + byte[] fileBytes = memoryStream.ToArray(); + + var jsonData = DecryptDataAES(fileBytes, Key, IV); + var mapData = JsonSerializer.Deserialize(jsonData, JsonOptionExtends.Read); + if (mapData is null) return new(false, "Dữ liệu không hợp lệ"); + else return new(true) { Data = mapData }; + } + catch (Exception ex) + { + Logger.Warning($"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"EncryptMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + private static byte[] EncryptDataAES(string data, byte[] key, byte[] iv) + { + using Aes aesAlg = Aes.Create(); + aesAlg.Key = key; + aesAlg.IV = iv; + + ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + + using MemoryStream msEncrypt = new(); + using CryptoStream csEncrypt = new(msEncrypt, encryptor, CryptoStreamMode.Write); + using (StreamWriter swEncrypt = new(csEncrypt)) + { + swEncrypt.Write(data); + } + return msEncrypt.ToArray(); + } + + private static string DecryptDataAES(byte[] data, byte[] key, byte[] iv) + { + using Aes aesAlg = Aes.Create(); + aesAlg.Key = key; + aesAlg.IV = iv; + + ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + + using MemoryStream msDecrypt = new(data); + using CryptoStream csDecrypt = new(msDecrypt, decryptor, CryptoStreamMode.Read); + using StreamReader srDecrypt = new(csDecrypt); + return srDecrypt.ReadToEnd(); + } + + public async Task GetImageDataAsync(bool usingLocal, string url) + { + if (!usingLocal) + { + var http = HttpClientFactory.CreateClient(); + var response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + if (!response.IsSuccessStatusCode) return []; + return await response.Content.ReadAsByteArrayAsync(); + } + else + { + if (System.IO.File.Exists(url)) return System.IO.File.ReadAllBytes(url); + return []; + } + } +} diff --git a/RobotNet.MapManager/Controllers/MapsDataController.cs b/RobotNet.MapManager/Controllers/MapsDataController.cs new file mode 100644 index 0000000..aff7394 --- /dev/null +++ b/RobotNet.MapManager/Controllers/MapsDataController.cs @@ -0,0 +1,381 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Models; +using RobotNet.Shares; +using System.Text.Json; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class MapsDataController(MapEditorDbContext MapDb, LoggerController Logger) : ControllerBase +{ + [HttpGet] + [Route("{id}")] + public async Task> GetMapData(Guid id) + { + var map = await MapDb.Maps.FindAsync(id); + if (map is null) return new(false, $"Không tìm thấy bản đồ: {id}"); + + var result = new MessageResult(true) + { + Data = new() + { + Id = map.Id, + Name = map.Name, + OriginX = map.OriginX, + OriginY = map.OriginY, + Resolution = map.Resolution, + ImageHeight = map.ImageHeight, + ImageWidth = map.ImageWidth, + Active = map.Active, + Nodes = [.. MapDb.Nodes.Where(node => node.MapId == id).Select(node => new NodeDto() + { + Id = node.Id, + MapId = node.MapId, + Name = node.Name, + Theta = node.Theta, + X = node.X, + Y = node.Y, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions, + })], + Edges = [.. MapDb.Edges.Where(edge => edge.MapId == id).Select(edge => new EdgeDto() + { + Id = edge.Id, + MapId = edge.MapId, + StartNodeId = edge.StartNodeId, + EndNodeId = edge.EndNodeId, + + MaxSpeed = edge.MaxSpeed, + MaxHeight = edge.MaxHeight, + MinHeight = edge.MinHeight, + DirectionAllowed = edge.DirectionAllowed, + RotationAllowed = edge.RotationAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + MaxRotationSpeed = edge.MaxRotationSpeed, + Actions = edge.Actions, + AllowedDeviationXy = edge.AllowedDeviationXy, + AllowedDeviationTheta = edge.AllowedDeviationTheta, + })], + Zones = [.. MapDb.Zones.Where(zone => zone.MapId == id).Select(zone => new ZoneDto() + { + Id = zone.Id, + MapId = zone.MapId, + Type = zone.Type, + Name = zone.Name, + X1 = zone.X1, + X2 = zone.X2, + Y1 = zone.Y1, + Y2 = zone.Y2, + X3 = zone.X3, + Y3 = zone.Y3, + X4 = zone.X4, + Y4 = zone.Y4, + }).OrderBy(z => z.Type)], + Elements = [.. MapDb.Elements.Where(el => el.MapId == id).Select(element => new ElementDto() + { + Id = element.Id, + MapId = element.MapId, + ModelId = element.ModelId, + Name = element.Name, + NodeId = element.NodeId, + OffsetX = element.OffsetX, + OffsetY = element.OffsetY, + IsOpen = element.IsOpen, + Content = element.Content, + })], + Actions = [.. MapDb.Actions.Where(a => a.MapId == id).Select(action => new ActionDto() + { + Id = action.Id, + MapId = action.MapId, + Name = action.Name, + Content = action.Content, + })] + } + }; + return result; + } + + [HttpPut] + [Route("{id}/updates")] + public async Task>> Updates(Guid id, MapEditorBackupModel model) + { + if (model == null || model.Steps == null) return new(false, "Dữ liệu đầu vào không hợp lệ"); + try + { + var map = await MapDb.Maps.FindAsync(id); + if (map == null) return new(false, $"Không tồn tại map id = {id}"); + + List EdgeDtos = []; + foreach (var step in model.Steps) + { + switch (step.Type) + { + case MapEditorBackupType.Node: + PositionBackup? nodeUpdate = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (nodeUpdate is not null) + { + var nodeDb = await MapDb.Nodes.FindAsync(step.Id); + if (nodeDb is not null) + { + nodeDb.X = nodeUpdate.X; + nodeDb.Y = nodeUpdate.Y; + } + } + break; + case MapEditorBackupType.ControlPoint1Edge: + PositionBackup? controlPoint1 = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (controlPoint1 is not null) + { + var edgeDb = await MapDb.Edges.FindAsync(step.Id); + if (edgeDb is not null) + { + edgeDb.ControlPoint1X = controlPoint1.X; + edgeDb.ControlPoint1Y = controlPoint1.Y; + } + } + break; + case MapEditorBackupType.ControlPoint2Edge: + PositionBackup? controlPoint2 = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (controlPoint2 is not null) + { + var edgeDb = await MapDb.Edges.FindAsync(step.Id); + if (edgeDb is not null) + { + edgeDb.ControlPoint2X = controlPoint2.X; + edgeDb.ControlPoint2Y = controlPoint2.Y; + } + } + break; + case MapEditorBackupType.Zone: + ZoneShapeBackup? zoneUpdate = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (zoneUpdate is not null) + { + var zoneDb = await MapDb.Zones.FindAsync(step.Id); + if (zoneDb is not null) + { + zoneDb.X1 = zoneUpdate.X1; + zoneDb.Y1 = zoneUpdate.Y1; + zoneDb.X2 = zoneUpdate.X2; + zoneDb.Y2 = zoneUpdate.Y2; + zoneDb.X3 = zoneUpdate.X3; + zoneDb.Y3 = zoneUpdate.Y3; + zoneDb.X4 = zoneUpdate.X4; + zoneDb.Y4 = zoneUpdate.Y4; + } + } + break; + case MapEditorBackupType.MoveEdge: + List? edgesUpdate = JsonSerializer.Deserialize>(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (edgesUpdate is not null && edgesUpdate.Count > 0) + { + foreach (var edgeUpate in edgesUpdate) + { + var edgeDb = await MapDb.Edges.FindAsync(edgeUpate.Id); + if (edgeDb is not null) + { + var startNode = await MapDb.Nodes.FindAsync(edgeDb.StartNodeId); + var endNode = await MapDb.Nodes.FindAsync(edgeDb.EndNodeId); + if (startNode is not null && endNode is not null) + { + startNode.X = edgeUpate.StartX; + startNode.Y = edgeUpate.StartY; + endNode.X = edgeUpate.EndX; + endNode.Y = edgeUpate.EndY; + } + edgeDb.ControlPoint1X = edgeUpate.ControlPoint1X; + edgeDb.ControlPoint1Y = edgeUpate.ControlPoint1Y; + edgeDb.ControlPoint2X = edgeUpate.ControlPoint2X; + edgeDb.ControlPoint2Y = edgeUpate.ControlPoint2Y; + } + } + } + break; + case MapEditorBackupType.Copy: + List? edgesCopy = JsonSerializer.Deserialize>(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (edgesCopy is not null && edgesCopy.Count > 0) + { + Dictionary CreateNewNode = []; + foreach (var edgeCopy in edgesCopy) + { + if (!CreateNewNode.TryGetValue(edgeCopy.StartNodeId, out _)) + { + var startNode = await MapDb.Nodes.FindAsync(edgeCopy.StartNodeId); + var newStartNode = await MapDb.Nodes.AddAsync(new Node() + { + MapId = edgeCopy.MapId, + Name = map.NodeNameAutoGenerate ? $"{map.NodeNameTemplateDefault}{++map.NodeCount}" : string.Empty, + X = edgeCopy.X1, + Y = edgeCopy.Y1, + Theta = startNode is not null ? startNode.Theta : 0, + Actions = startNode is not null ? startNode.Actions : "", + AllowedDeviationXy = startNode is not null ? startNode.AllowedDeviationXy : map.NodeAllowedDeviationXyDefault, + AllowedDeviationTheta = startNode is not null ? startNode.AllowedDeviationTheta : map.NodeAllowedDeviationThetaDefault, + }); + CreateNewNode.Add(edgeCopy.StartNodeId, newStartNode.Entity); + } + if (!CreateNewNode.TryGetValue(edgeCopy.EndNodeId, out _)) + { + var endNode = await MapDb.Nodes.FindAsync(edgeCopy.EndNodeId); + var newEndNode = await MapDb.Nodes.AddAsync(new Node() + { + MapId = edgeCopy.MapId, + Name = map.NodeNameAutoGenerate ? $"{map.NodeNameTemplateDefault}{++map.NodeCount}" : string.Empty, + X = edgeCopy.X2, + Y = edgeCopy.Y2, + Theta = endNode is not null ? endNode.Theta : 0, + Actions = endNode is not null ? endNode.Actions : "", + AllowedDeviationXy = endNode is not null ? endNode.AllowedDeviationXy : map.NodeAllowedDeviationXyDefault, + AllowedDeviationTheta = endNode is not null ? endNode.AllowedDeviationTheta : map.NodeAllowedDeviationThetaDefault, + }); + CreateNewNode.Add(edgeCopy.EndNodeId, newEndNode.Entity); + } + + var newEdge = await MapDb.Edges.AddAsync(new Edge() + { + MapId = edgeCopy.MapId, + StartNodeId = CreateNewNode[edgeCopy.StartNodeId] is null ? Guid.Empty : CreateNewNode[edgeCopy.StartNodeId].Id, + EndNodeId = CreateNewNode[edgeCopy.EndNodeId] is null ? Guid.Empty : CreateNewNode[edgeCopy.EndNodeId].Id, + TrajectoryDegree = edgeCopy.TrajectoryDegree, + ControlPoint1X = edgeCopy.ControlPoint1X, + ControlPoint1Y = edgeCopy.ControlPoint1Y, + ControlPoint2X = edgeCopy.ControlPoint2X, + ControlPoint2Y = edgeCopy.ControlPoint2Y, + DirectionAllowed = edgeCopy.DirectionAllowed, + RotationAllowed = edgeCopy.RotationAllowed, + MaxSpeed = edgeCopy.MaxSpeed, + MaxRotationSpeed = edgeCopy.MaxRotationSpeed, + MaxHeight = edgeCopy.MaxHeight, + MinHeight = edgeCopy.MinHeight, + Actions = edgeCopy.Actions, + AllowedDeviationTheta = edgeCopy.AllowedDeviationTheta, + AllowedDeviationXy = edgeCopy.AllowedDeviationXy, + }); + EdgeDtos.Add(new() + { + + Id = newEdge.Entity.Id, + MapId = newEdge.Entity.MapId, + StartNodeId = newEdge.Entity.StartNodeId, + EndNodeId = newEdge.Entity.EndNodeId, + TrajectoryDegree = newEdge.Entity.TrajectoryDegree, + ControlPoint1X = newEdge.Entity.ControlPoint1X, + ControlPoint1Y = newEdge.Entity.ControlPoint1Y, + ControlPoint2X = newEdge.Entity.ControlPoint2X, + ControlPoint2Y = newEdge.Entity.ControlPoint2Y, + DirectionAllowed = newEdge.Entity.DirectionAllowed, + RotationAllowed = newEdge.Entity.RotationAllowed, + MaxSpeed = newEdge.Entity.MaxSpeed, + MaxRotationSpeed = newEdge.Entity.MaxRotationSpeed, + MaxHeight = newEdge.Entity.MaxHeight, + MinHeight = newEdge.Entity.MinHeight, + Actions = newEdge.Entity.Actions, + AllowedDeviationXy = newEdge.Entity.AllowedDeviationXy, + AllowedDeviationTheta = newEdge.Entity.AllowedDeviationTheta, + StartNode = new NodeDto() + { + Id = CreateNewNode[edgeCopy.StartNodeId].Id, + Name = CreateNewNode[edgeCopy.StartNodeId].Name, + MapId = CreateNewNode[edgeCopy.StartNodeId].MapId, + Theta = CreateNewNode[edgeCopy.StartNodeId].Theta, + X = CreateNewNode[edgeCopy.StartNodeId].X, + Y = CreateNewNode[edgeCopy.StartNodeId].Y, + AllowedDeviationXy = CreateNewNode[edgeCopy.StartNodeId].AllowedDeviationXy, + AllowedDeviationTheta = CreateNewNode[edgeCopy.StartNodeId].AllowedDeviationTheta, + Actions = CreateNewNode[edgeCopy.StartNodeId].Actions, + }, + EndNode = new NodeDto() + { + Id = CreateNewNode[edgeCopy.EndNodeId].Id, + Name = CreateNewNode[edgeCopy.EndNodeId].Name, + MapId = CreateNewNode[edgeCopy.EndNodeId].MapId, + Theta = CreateNewNode[edgeCopy.EndNodeId].Theta, + X = CreateNewNode[edgeCopy.EndNodeId].X, + Y = CreateNewNode[edgeCopy.EndNodeId].Y, + AllowedDeviationXy = CreateNewNode[edgeCopy.EndNodeId].AllowedDeviationXy, + AllowedDeviationTheta = CreateNewNode[edgeCopy.EndNodeId].AllowedDeviationTheta, + Actions = CreateNewNode[edgeCopy.EndNodeId].Actions, + }, + }); + } + } + break; + case MapEditorBackupType.SplitNode: + var nodeSplit = await MapDb.Nodes.FindAsync(step.Id); + if (nodeSplit is not null) + { + SplitNodeBackup? SplitNodeUpdate = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (SplitNodeUpdate is not null) + { + foreach (var data in SplitNodeUpdate.EdgeSplit) + { + var edge = await MapDb.Edges.FindAsync(data.Key); + if (edge is not null) + { + var newNode = new Node() + { + Id = data.Value.Id, + Name = data.Value.Name, + X = data.Value.X, + Y = data.Value.Y, + Theta = data.Value.Theta, + MapId = data.Value.MapId, + AllowedDeviationXy = data.Value.AllowedDeviationXy, + AllowedDeviationTheta = data.Value.AllowedDeviationTheta, + Actions = data.Value.Actions, + }; + if (edge.StartNodeId == nodeSplit.Id) edge.StartNodeId = newNode.Id; + else if (edge.EndNodeId == nodeSplit.Id) edge.EndNodeId = newNode.Id; + else continue; + await MapDb.Nodes.AddAsync(newNode); + } + } + } + } + break; + case MapEditorBackupType.MergeNode: + var nodemerge = await MapDb.Nodes.FindAsync(step.Id); + if (nodemerge is not null) + { + MergeNodeUpdate? MergeNodeUpdate = JsonSerializer.Deserialize(step.Obj.ToString() ?? "", JsonOptionExtends.Read); + if (MergeNodeUpdate is not null) + { + foreach (var data in MergeNodeUpdate.EdgesMerge) + { + var edge = await MapDb.Edges.FindAsync(data.Key); + if (edge is not null) + { + var rmNode = await MapDb.Nodes.FindAsync(data.Value); + if (edge.StartNodeId == data.Value) edge.StartNodeId = nodemerge.Id; + else if (edge.EndNodeId == data.Value) edge.EndNodeId = nodemerge.Id; + if (rmNode is not null) MapDb.Nodes.Remove(rmNode); + } + } + } + } + break; + } + } + + await MapDb.SaveChangesAsync(); + Logger.Info($"User {HttpContext.User.Identity?.Name} đã cập nhật dữ liệu cho bản đồ: {map.Name} - {map.Id}"); + return new(true) { Data = EdgeDtos }; + } + catch (Exception ex) + { + Logger.Warning($"Updates: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/MapsManagerController.cs b/RobotNet.MapManager/Controllers/MapsManagerController.cs new file mode 100644 index 0000000..673f73d --- /dev/null +++ b/RobotNet.MapManager/Controllers/MapsManagerController.cs @@ -0,0 +1,571 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Hubs; +using RobotNet.MapManager.Services; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class MapsManagerController(MapEditorDbContext MapDb, MapEditorStorageRepository StorageRepo, IHubContext MapHub, LoggerController Logger) : ControllerBase +{ + [HttpGet] + public async Task> GetMapInfos([FromQuery(Name = "txtSearch")] string? txtSearch) + { + try + { + if (string.IsNullOrWhiteSpace(txtSearch)) + { + return await (from map in MapDb.Maps + select new MapInfoDto() + { + Id = map.Id, + VersionId = map.VersionId, + Name = map.Name, + Description = map.Description, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + VDA5050 = map.VDA5050, + }).ToListAsync(); + } + else + { + return await (from map in MapDb.Maps + where !string.IsNullOrEmpty(map.Name) && map.Name.Contains(txtSearch) + select new MapInfoDto() + { + Id = map.Id, + VersionId = map.VersionId, + Name = map.Name, + Description = map.Description, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + VDA5050 = map.VDA5050, + }).ToListAsync(); + } + } + catch (Exception ex) + { + Logger.Warning($"GetMapInfos: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + } + + [HttpGet] + [Route("{id}")] + public async Task> GetMapInfoId(Guid id) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == id); + if (map == null) return new MessageResult(false, $"Không tìm thấy map {id}"); + + return new(true) + { + Data = new MapInfoDto() + { + Id = map.Id, + Name = map.Name, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + }, + }; + } + + [HttpGet] + [Route("info")] + public async Task> GetMapInfoName([FromQuery(Name = "name")] string name) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == name); + if (map == null) return new MessageResult(false, $"Không tìm thấy map {name}"); + + return new(true) + { + Data = new MapInfoDto() + { + Id = map.Id, + Name = map.Name, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + }, + }; + } + + [HttpPost] + [Route("")] + public async Task> CreateMap([FromForm] MapCreateModel model, [FromForm(Name = "Image")] IFormFile imageUpload) + { + try + { + if (imageUpload is null) return new(false, "Dữ liệu không hợp lệ"); + if (imageUpload.ContentType != "image/png") return new(false, "Ảnh map chỉ hỗ trợ định dạng image/png"); + if (await MapDb.Maps.AnyAsync(map => map.Name == model.Name)) return new(false, "Tên của map đã tồn tại. Hãy đặt tên khác!"); + + var image = SixLabors.ImageSharp.Image.Load(imageUpload.OpenReadStream()); + + var entityMap = await MapDb.Maps.AddAsync(new Map() + { + Name = model.Name, + OriginX = model.OriginX, + OriginY = model.OriginY, + Resolution = model.Resolution, + ImageHeight = (ushort)image.Height, + ImageWidth = (ushort)image.Width, + Active = false, + + NodeCount = 0, + NodeNameAutoGenerate = true, + NodeNameTemplateDefault = "Node", + NodeAllowedDeviationXyDefault = 0.1, + NodeAllowedDeviationThetaDefault = 0, + + EdgeMinLengthDefault = 1, + EdgeMaxHeightDefault = 1, + EdgeMinHeightDefault = 0.1, + EdgeStraightMaxSpeedDefault = 1, + EdgeCurveMaxSpeedDefault = 0.3, + EdgeMaxRotationSpeedDefault = 0.5, + EdgeAllowedDeviationXyDefault = 0.1, + EdgeAllowedDeviationThetaDefault = 0, + EdgeRotationAllowedDefault = true, + EdgeDirectionAllowedDefault = DirectionAllowed.Both, + + ZoneMinSquareDefault = 0.25, + }); + + await MapDb.SaveChangesAsync(); + + var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{entityMap.Entity.Id}", imageUpload.OpenReadStream(), imageUpload.Length, imageUpload.ContentType, CancellationToken.None); + if (!isSuccess) + { + MapDb.Maps.Remove(entityMap.Entity); + await MapDb.SaveChangesAsync(); + return new(false, message); + } + + await MapDb.SaveChangesAsync(); + Logger.Info($"User {HttpContext.User.Identity?.Name} đã tạo bản đồ mới với tên: {model.Name}"); + + return new(true) + { + Data = new MapInfoDto() + { + Id = entityMap.Entity.Id, + Name = entityMap.Entity.Name, + Active = entityMap.Entity.Active, + OriginX = entityMap.Entity.OriginX, + OriginY = entityMap.Entity.OriginY, + Width = entityMap.Entity.ImageWidth * entityMap.Entity.Resolution, + Height = entityMap.Entity.ImageHeight * entityMap.Entity.Resolution, + Resolution = entityMap.Entity.Resolution, + }, + }; + } + catch (Exception ex) + { + Logger.Warning($"CreateMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"CreateMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + [Route("{id}")] + public async Task> UpdateMap(Guid id, [FromBody] MapUpdateModel model) + { + try + { + if (id != model.Id) return new(false, "Dữ liệu gửi không chính xác"); + + var map = await MapDb.Maps.FindAsync(id); + if (map == null) return new(false, $"Không tồn tại map id = {id}"); + + if (map.Name != model.Name && await MapDb.Maps.AnyAsync(m => m.Name == model.Name)) + { + return new(false, "Tên của map đã tồn tại, Hãy đặt tên khác!"); + } + + if (model.Resolution <= 0) + { + return new(false, "Độ phân giải của bản đồ phải lớn hơn 0"); + } + + map.Name = model.Name; + bool originChange = map.OriginX != model.OriginX || map.OriginY != model.OriginY; + if (originChange) + { + map.OriginX = model.OriginX; + map.OriginY = model.OriginY; + } + if (map.Resolution != model.Resolution) + { + var scale = model.Resolution / map.Resolution; + map.Resolution = model.Resolution; + if (originChange) + { + map.OriginX *= scale; + map.OriginY *= scale; + } + + var nodes = await MapDb.Nodes.Where(n => n.MapId == map.Id).ToListAsync(); + foreach (var node in nodes) + { + node.X *= scale; + node.Y *= scale; + } + + var edges = await MapDb.Edges.Where(e => e.MapId == map.Id).ToListAsync(); + foreach (var edge in edges) + { + edge.ControlPoint1X *= scale; + edge.ControlPoint1Y *= scale; + edge.ControlPoint2X *= scale; + edge.ControlPoint2Y *= scale; + } + } + + await MapDb.SaveChangesAsync(); + Logger.Info($"User {HttpContext.User.Identity?.Name} đã cập nhật thông tin bản đồ : {model.Id} - {map.Name}"); + + return new(true) + { + Data = new() + { + Id = id, + Name = map.Name, + OriginX = map.OriginX, + OriginY = map.OriginY, + Resolution = model.Resolution, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + }, + }; + } + catch (Exception ex) + { + Logger.Warning($"UpdateMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task DeleteMap(Guid id) + { + try + { + var map = await MapDb.Maps.FindAsync(id); + if (map == null) return new(false, $"Không tồn tại map id = {id}"); + + MapDb.Elements.RemoveRange(MapDb.Elements.Where(e => e.MapId == map.Id)); + MapDb.ElementModels.RemoveRange(MapDb.ElementModels.Where(em => em.MapId == map.Id)); + MapDb.Edges.RemoveRange(MapDb.Edges.Where(edge => edge.MapId == map.Id)); + MapDb.Nodes.RemoveRange(MapDb.Nodes.Where(node => node.MapId == map.Id)); + MapDb.Zones.RemoveRange(MapDb.Zones.Where(zone => zone.MapId == map.Id)); + MapDb.Actions.RemoveRange(MapDb.Actions.Where(action => action.MapId == map.Id)); + MapDb.Maps.Remove(map); + await MapDb.SaveChangesAsync(); + + await StorageRepo.DeleteAsync("MapImages", $"{id}", CancellationToken.None); + + Logger.Info($"User {HttpContext.User.Identity?.Name} đã xóa bản đồ {map.Name}"); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"DeleteMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"DeleteMap {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPut] + [Route("active")] + public async Task ActiveToggle([FromBody] MapActiveModel model) + { + var map = await MapDb.Maps.FindAsync(model.Id); + if (map == null) return new(false, $"Không tồn tại map id = {model.Id}"); + + map.Active = model.Active; + await MapDb.SaveChangesAsync(); + + await MapHub.Clients.All.SendAsync("MapUpdated", model.Id); + + Logger.Info($"User {HttpContext.User.Identity?.Name} đã thay đổi trạng thái active bản đồ {map.Name}: {model.Active}"); + + return new(true); + } + + [HttpPut] + [Route("image/{id}")] + public async Task UpdateImage(Guid id, [FromForm(Name = "image")] IFormFile image) + { + try + { + var map = await MapDb.Maps.FindAsync(id); + if (map == null) return new(false, $"Không tồn tại map id = {id}"); + + var imageStream = image.OpenReadStream(); + var imageUpdate = SixLabors.ImageSharp.Image.Load(imageStream); + map.ImageWidth = (ushort)imageUpdate.Width; + map.ImageHeight = (ushort)imageUpdate.Height; + await MapDb.SaveChangesAsync(); + var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{id}", image.OpenReadStream(), image.Length, image.ContentType, CancellationToken.None); + + Logger.Info($"User {HttpContext.User.Identity?.Name} đã thay đổi ảnh của bản đồ {map.Name}"); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"UpdateImage {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPost] + [Route("import")] + public async Task> ImportNewMap([FromBody] MapExportDto model) + { + if (model == null || model.Data == null || model.Data.ImageData == null || model.Data.ImageData.Length == 0) + { + return new(false, "Dữ liệu đầu vào không hợp lệ"); + } + + using var transaction = await MapDb.Database.BeginTransactionAsync(); + try + { + if (await MapDb.Maps.AnyAsync(map => map.Name == model.Name)) return new(false, "Tên của map đã tồn tại, Hãy đặt tên khác!"); + + using var stream = new MemoryStream(model.Data.ImageData); + var imageFile = new FormFile(stream, 0, model.Data.ImageData.Length, "", $"{model.Name}.png") + { + Headers = new HeaderDictionary(), + ContentType = "image/png", + }; + var image = SixLabors.ImageSharp.Image.Load(imageFile.OpenReadStream()); + + var entityMap = await MapDb.Maps.AddAsync(new Map() + { + Name = model.Name, + Description = model.Description, + OriginX = model.Info.OriginX, + OriginY = model.Info.OriginY, + Resolution = model.Info.Resolution, + ImageHeight = (ushort)image.Height, + ImageWidth = (ushort)image.Width, + Active = false, + ViewX = model.Info.ViewX, + ViewY = model.Info.ViewY, + ViewWidth = model.Info.ViewWidth, + ViewHeight = model.Info.ViewHeight, + VDA5050 = model.Info.VDA5050, + + NodeCount = model.Data.NodeCount, + NodeNameAutoGenerate = model.Setting.NodeNameAutoGenerate, + NodeNameTemplateDefault = model.Setting.NodeNameTemplate, + NodeAllowedDeviationXyDefault = model.Setting.NodeAllowedDeviationXy, + NodeAllowedDeviationThetaDefault = model.Setting.NodeAllowedDeviationTheta, + + EdgeMinLengthDefault = model.Setting.EdgeMinLength, + EdgeMaxHeightDefault = model.Setting.EdgeMaxHeight, + EdgeMinHeightDefault = model.Setting.EdgeMinHeight, + EdgeStraightMaxSpeedDefault = model.Setting.EdgeStraightMaxSpeed, + EdgeCurveMaxSpeedDefault = model.Setting.EdgeCurveMaxSpeed, + EdgeMaxRotationSpeedDefault = model.Setting.EdgeMaxRotationSpeed, + EdgeAllowedDeviationXyDefault = model.Setting.EdgeAllowedDeviationXy, + EdgeAllowedDeviationThetaDefault = model.Setting.EdgeAllowedDeviationTheta, + EdgeRotationAllowedDefault = model.Setting.EdgeRotationAllowed, + EdgeDirectionAllowedDefault = model.Setting.EdgeDirectionAllowed, + + ZoneMinSquareDefault = model.Setting.ZoneMinSquare, + }); + + var (isSuccess, message) = await StorageRepo.UploadAsync("MapImages", $"{entityMap.Entity.Id}", imageFile.OpenReadStream(), imageFile.Length, imageFile.ContentType, CancellationToken.None); + if (!isSuccess) + { + await transaction.RollbackAsync(); + return new(false, message); + } + + Dictionary actionSwap = []; + foreach (var action in model.Data.Actions) + { + var actionDb = await MapDb.Actions.AddAsync(new Data.Action() + { + MapId = entityMap.Entity.Id, + Name = action.Name, + Content = action.Content, + }); + actionSwap.Add(action.Id, actionDb.Entity.Id); + } + + Dictionary nodeSwap = []; + foreach (var node in model.Data.Nodes) + { + var actions = System.Text.Json.JsonSerializer.Deserialize(node.Actions ?? ""); + List newActions = []; + if (actions is not null && actions.Length > 0) + { + foreach (var actionId in actions) + { + if (actionSwap.TryGetValue(actionId, out Guid newActionId) && newActionId != Guid.Empty) newActions.Add(newActionId); + } + } + var nodeDb = await MapDb.Nodes.AddAsync(new Node() + { + Name = node.Name, + MapId = entityMap.Entity.Id, + X = node.X, + Y = node.Y, + Theta = node.Theta, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = System.Text.Json.JsonSerializer.Serialize(newActions), + }); + nodeSwap.Add(node.Id, nodeDb.Entity.Id); + } + + var Edges = model.Data.Edges.Select(e => new Edge() + { + MapId = entityMap.Entity.Id, + StartNodeId = nodeSwap[e.StartNodeId], + EndNodeId = nodeSwap[e.EndNodeId], + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + TrajectoryDegree = e.TrajectoryDegree, + MaxHeight = e.MaxHeight, + MinHeight = e.MinHeight, + MaxSpeed = e.MaxSpeed, + MaxRotationSpeed = e.MaxRotationSpeed, + AllowedDeviationXy = e.AllowedDeviationXy, + AllowedDeviationTheta = e.AllowedDeviationTheta, + DirectionAllowed = e.DirectionAllowed, + RotationAllowed = e.RotationAllowed, + Actions = e.Actions, + }).ToList(); + + var Zones = model.Data.Zones.Select(z => new Zone() + { + MapId = entityMap.Entity.Id, + Type = z.Type, + X1 = z.X1, + X2 = z.X2, + Y1 = z.Y1, + Y2 = z.Y2, + X3 = z.X3, + X4 = z.X4, + Y3 = z.Y3, + Y4 = z.Y4, + }).ToList(); + + Dictionary elementModelSwap = []; + foreach (var elementModel in model.Data.ElementModels) + { + var elementModelDb = await MapDb.ElementModels.AddAsync(new ElementModel() + { + MapId = entityMap.Entity.Id, + Name = elementModel.Name, + Height = elementModel.Height, + Width = elementModel.Width, + Image1Height = elementModel.Image1Height, + Image1Width = elementModel.Image1Width, + Image2Height = elementModel.Image2Height, + Image2Width = elementModel.Image2Width, + Content = elementModel.Content, + }); + elementModelSwap.Add(elementModel.Id, elementModelDb.Entity.Id); + + using var openStream = new MemoryStream(elementModel.ImageOpenData); + var imageOpenFile = new FormFile(openStream, 0, elementModel.ImageOpenData.Length, "", $"{elementModel.Name}O.png") + { + Headers = new HeaderDictionary(), + ContentType = "image/png", + }; + await StorageRepo.UploadAsync("ElementOpenModels", $"{elementModelDb.Entity.Id}", imageOpenFile.OpenReadStream(), imageOpenFile.Length, imageOpenFile.ContentType, CancellationToken.None); + + using var closeStream = new MemoryStream(elementModel.ImageCloseData); + var imageCloseFile = new FormFile(closeStream, 0, elementModel.ImageCloseData.Length, "", $"{elementModel.Name}C.png") + { + Headers = new HeaderDictionary(), + ContentType = "image/png", + }; + await StorageRepo.UploadAsync("ElementCloseModels", $"{elementModelDb.Entity.Id}", imageCloseFile.OpenReadStream(), imageCloseFile.Length, imageCloseFile.ContentType, CancellationToken.None); + } + + var Elements = model.Data.Elements.Select(e => new Data.Element() + { + MapId = entityMap.Entity.Id, + Name = e.Name, + IsOpen = e.IsOpen, + ModelId = elementModelSwap[e.ModelId], + Content = e.Content, + NodeId = nodeSwap[e.NodeId], + OffsetX = e.OffsetX, + OffsetY = e.OffsetY, + }).ToList(); + + if (Edges.Count > 0) await MapDb.Edges.AddRangeAsync(Edges); + if (Zones.Count > 0) await MapDb.Zones.AddRangeAsync(Zones); + if (Elements.Count > 0) await MapDb.Elements.AddRangeAsync(Elements); + + await MapDb.SaveChangesAsync(); + await transaction.CommitAsync(); + + return new(true) + { + Data = new MapInfoDto() + { + Id = entityMap.Entity.Id, + Name = entityMap.Entity.Name, + Active = entityMap.Entity.Active, + OriginX = entityMap.Entity.OriginX, + OriginY = entityMap.Entity.OriginY, + Width = entityMap.Entity.ImageWidth * entityMap.Entity.Resolution, + Height = entityMap.Entity.ImageHeight * entityMap.Entity.Resolution, + Resolution = entityMap.Entity.Resolution, + } + }; + } + catch (IOException ex) + { + await transaction.RollbackAsync(); + Logger.Warning($"ImportNewMap: Lỗi khi xử lý hình ảnh: {ex.Message}"); + return new(false, $"ImportNewMap: Lỗi khi xử lý hình ảnh: {ex.Message}"); + } + catch (DbUpdateException ex) + { + await transaction.RollbackAsync(); + Logger.Warning($"ImportNewMap: Lỗi khi lưu vào database: {ex.Message}"); + return new(false, $"ImportNewMap: Lỗi khi lưu vào database: {ex.Message}"); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + Logger.Warning($"ImportNewMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"ImportNewMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/MapsSettingController.cs b/RobotNet.MapManager/Controllers/MapsSettingController.cs new file mode 100644 index 0000000..f85e907 --- /dev/null +++ b/RobotNet.MapManager/Controllers/MapsSettingController.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class MapsSettingController(MapEditorDbContext MapDb) : ControllerBase +{ + [HttpGet] + [Route("{id}")] + public async Task> GetMapSetting(Guid id) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == id); + if (map == null) return new(false, $"Không tìm thấy map {id}"); + + return new(true) + { + Data = new MapSettingDefaultDto() + { + Id = map.Id, + EdgeStraightMaxSpeed = map.EdgeStraightMaxSpeedDefault, + EdgeCurveMaxSpeed = map.EdgeCurveMaxSpeedDefault, + EdgeMaxHeight = map.EdgeMaxHeightDefault, + EdgeMinHeight = map.EdgeMinHeightDefault, + EdgeMinLength = map.EdgeMinLengthDefault, + EdgeDirectionAllowed = map.EdgeDirectionAllowedDefault, + EdgeMaxRotationSpeed = map.EdgeMaxRotationSpeedDefault, + EdgeRotationAllowed = map.EdgeRotationAllowedDefault, + EdgeAllowedDeviationXy = map.EdgeAllowedDeviationXyDefault, + EdgeAllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault, + + NodeAllowedDeviationTheta = map.NodeAllowedDeviationThetaDefault, + NodeAllowedDeviationXy = map.NodeAllowedDeviationXyDefault, + NodeNameAutoGenerate = map.NodeNameAutoGenerate, + NodeNameTemplate = map.NodeNameTemplateDefault, + + ZoneMinSquare = map.ZoneMinSquareDefault, + }, + }; + } + + + [HttpPut] + [Route("")] + public async Task Update([FromBody] MapSettingDefaultDto mapSetting) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == mapSetting.Id); + if (map == null) return new(false, $"Không tìm thấy map {mapSetting.Id}"); + + map.EdgeStraightMaxSpeedDefault = mapSetting.EdgeStraightMaxSpeed; + map.EdgeCurveMaxSpeedDefault = mapSetting.EdgeCurveMaxSpeed; + map.EdgeMaxHeightDefault = mapSetting.EdgeMaxHeight; + map.EdgeMinHeightDefault = mapSetting.EdgeMinHeight; + map.EdgeMinLengthDefault = mapSetting.EdgeMinLength; + map.EdgeDirectionAllowedDefault = mapSetting.EdgeDirectionAllowed; + map.EdgeMaxRotationSpeedDefault = mapSetting.EdgeMaxRotationSpeed; + map.EdgeRotationAllowedDefault = mapSetting.EdgeRotationAllowed; + map.EdgeAllowedDeviationXyDefault = mapSetting.EdgeAllowedDeviationXy; + map.EdgeAllowedDeviationThetaDefault = mapSetting.EdgeAllowedDeviationTheta; + + map.NodeAllowedDeviationThetaDefault = mapSetting.NodeAllowedDeviationTheta; + map.NodeAllowedDeviationXyDefault = mapSetting.NodeAllowedDeviationXy; + map.NodeNameAutoGenerate = mapSetting.NodeNameAutoGenerate; + map.NodeNameTemplateDefault = mapSetting.NodeNameTemplate; + + map.ZoneMinSquareDefault = mapSetting.ZoneMinSquare; + + MapDb.Maps.Update(map); + await MapDb.SaveChangesAsync(); + + return new(true); + } +} diff --git a/RobotNet.MapManager/Controllers/NodesController.cs b/RobotNet.MapManager/Controllers/NodesController.cs new file mode 100644 index 0000000..182774e --- /dev/null +++ b/RobotNet.MapManager/Controllers/NodesController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class NodesController(MapEditorDbContext MapDb) : ControllerBase +{ + [HttpGet] + [Route("{mapId}")] + public async Task GetNodes(Guid mapId) + { + return await (from node in MapDb.Nodes + where node.MapId == mapId + select new NodeDto() + { + Id = node.Id, + Name = node.Name, + MapId = mapId, + X = node.X, + Y = node.Y, + Theta = node.Theta, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions, + }).ToArrayAsync(); + } + + [HttpPut] + [Route("")] + public async Task Update([FromBody] NodeUpdateModel model) + { + var node = await MapDb.Nodes.FindAsync(model.Id); + if (node == null) return new(false, $"Không tồn tại node id = {model.Id}"); + + if (node.Name != model.Name && !string.IsNullOrWhiteSpace(model.Name) && await MapDb.Nodes.AnyAsync(n => n.Name == model.Name && n.MapId == node.MapId)) + { + return new(false, $"Tên node {model.Name} đã tồn tại trong map"); + } + + node.Name = model.Name; + node.X = model.X; + node.Y = model.Y; + node.Theta = model.Theta; + node.AllowedDeviationXy = model.AllowedDeviationXy; + node.AllowedDeviationTheta = model.AllowedDeviationTheta; + node.Actions = System.Text.Json.JsonSerializer.Serialize(model.Actions ?? []); + + await MapDb.SaveChangesAsync(); + return new(true); + } +} \ No newline at end of file diff --git a/RobotNet.MapManager/Controllers/ScriptElementsController.cs b/RobotNet.MapManager/Controllers/ScriptElementsController.cs new file mode 100644 index 0000000..01e18da --- /dev/null +++ b/RobotNet.MapManager/Controllers/ScriptElementsController.cs @@ -0,0 +1,232 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Models; +using RobotNet.MapShares.Property; +using RobotNet.Shares; +using Serialize.Linq.Serializers; +using System.Linq.Expressions; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ScriptElementsController(MapEditorDbContext MapDb, LoggerController Logger) : ControllerBase +{ + private static readonly ExpressionSerializer expressionSerializer; + + static ScriptElementsController() + { + var jss = new Serialize.Linq.Serializers.JsonSerializer(); + expressionSerializer = new ExpressionSerializer(jss); + + expressionSerializer.AddKnownType(typeof(Script.Expressions.ElementProperties)); + } + + [HttpPost] + public async Task>> GetElementsWithCondition([FromBody] ElementExpressionModel model) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == model.MapName); + if (map is null) return new(false, $"Không tồn tại map tên = {model.MapName}"); + + var elModel = await MapDb.ElementModels.FirstOrDefaultAsync(m => m.MapId == map.Id && m.Name == model.ModelName); + if (elModel is null) return new(false, $"Không tồn tại element model tên = {model.ModelName}"); + + var modelProperties = System.Text.Json.JsonSerializer.Deserialize>(elModel.Content, JsonOptionExtends.Read); + if (modelProperties is null || modelProperties.Count == 0) + return new(false, $"Không tồn tại property nào trong element model tên = {model.ModelName}"); + + var expr = expressionSerializer.DeserializeText(model.Expression); + var lambda = (Expression>)expr; + + // Compile và chạy: + var func = lambda.Compile(); + + var elements = await MapDb.Elements.Where(e => e.MapId == map.Id && e.ModelId == elModel.Id).ToListAsync(); + List elementSatisfies = []; + foreach (var element in elements) + { + var properties = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content); + if (func.Invoke(properties)) + { + var elNode = await MapDb.Nodes.FindAsync(element.NodeId); + if (elNode is null) continue; // Bỏ qua nếu không tìm thấy node + elementSatisfies.Add(new ElementDto() + { + Id = element.Id, + Name = element.Name, + MapId = element.MapId, + IsOpen = element.IsOpen, + NodeId = element.NodeId, + OffsetX = element.OffsetX, + OffsetY = element.OffsetY, + ModelId = element.ModelId, + Content = element.Content, + NodeName = elNode.Name, + ModelName = elModel.Name, + X = elNode.X, + Y = elNode.Y, + Theta = elNode.Theta + }); + } + } + + return new(true) + { + Data = elementSatisfies + }; + } + catch (Exception ex) + { + Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex}"); + return new(false, $"Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpGet] + [Route("{mapName}/node/{nodeName}")] + public async Task> GetNode([FromRoute] string mapName, [FromRoute] string nodeName) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName); + if (map is null) return new(false, $"Không tồn tại map tên = {mapName}"); + + var node = await MapDb.Nodes.FirstOrDefaultAsync(n => n.MapId == map.Id && n.Name == nodeName); + if (node is null) return new(false, $"Không tồn tại node {nodeName} trong map {mapName}"); + + return new(true) + { + Data = new NodeDto() + { + Id = node.Id, + Name = node.Name, + MapId = node.MapId, + X = node.X, + Y = node.Y, + Theta = node.Theta, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpGet] + [Route("{mapName}/element/{elementName}")] + public async Task> GetElement([FromRoute] string mapName, [FromRoute] string elementName) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName); + if (map is null) return new(false, $"Không tồn tại map tên = {mapName}"); + + var el = await MapDb.Elements.FirstOrDefaultAsync(e => e.MapId == map.Id && e.Name == elementName); + if (el is null) return new(false, $"Không tồn tại element name = {elementName}"); + + var elModel = await MapDb.ElementModels.FindAsync(el.ModelId); + if (elModel == null) return new(false, $"Không tồn tại element model id = {el.ModelId}"); + + var elNode = await MapDb.Nodes.FindAsync(el.NodeId); + if (elNode is null) return new(false, $"Không tồn tại node id = {el.NodeId}"); + + return new(true) + { + Data = new() + { + Id = el.Id, + Name = el.Name, + MapId = el.MapId, + IsOpen = el.IsOpen, + NodeId = el.NodeId, + OffsetX = el.OffsetX, + OffsetY = el.OffsetY, + ModelId = el.ModelId, + Content = el.Content, + NodeName = elNode.Name, + ModelName = elModel.Name, + X = elNode.X, + Y = elNode.Y, + Theta = elNode.Theta + } + }; + } + catch (Exception ex) + { + Logger.Warning($"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"GetElement: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPatch] + [Route("{mapName}/element/{elementName}")] + public async Task UpdateElementProperty([FromRoute] string mapName, [FromRoute] string elementName, [FromBody] ElementPropertyUpdateModel model) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName); + if (map is null) return new(false, $"Không tồn tại map tên = {mapName}"); + + var element = await MapDb.Elements.FirstOrDefaultAsync(m => m.Name == elementName && m.MapId == map.Id); + if (element == null) return new(false, $"Không tồn tại element tên = {elementName} trong map tên = {mapName}"); + + var properties = System.Text.Json.JsonSerializer.Deserialize>(element.Content, JsonOptionExtends.Read); + foreach (var property in model.Properties) + { + var existingProperty = properties?.FirstOrDefault(p => p.Name == property.Name); + if (existingProperty != null) + { + existingProperty.DefaultValue = property.DefaultValue; + } + else return new(false, $"Không tồn tại property name = {property.Name} trong element"); + } + var content = System.Text.Json.JsonSerializer.Serialize(properties, JsonOptionExtends.Write); + element.Content = content; + await MapDb.SaveChangesAsync(); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } + + [HttpPatch] + [Route("{mapName}/element/{elementName}/IsOpen")] + public async Task UpdateOpenOfElement([FromRoute] string mapName, [FromRoute] string elementName, [FromQuery] bool isOpen) + { + try + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == mapName); + if (map is null) return new(false, $"Không tồn tại map tên = {mapName}"); + + var element = await MapDb.Elements.FirstOrDefaultAsync(m => m.Name == elementName && m.MapId == map.Id); + if (element == null) return new(false, $"Không tồn tại element tên = {elementName} trong map tên = {mapName}"); + + element.IsOpen = isOpen; + await MapDb.SaveChangesAsync(); + + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Controllers/ZonesController.cs b/RobotNet.MapManager/Controllers/ZonesController.cs new file mode 100644 index 0000000..29d8427 --- /dev/null +++ b/RobotNet.MapManager/Controllers/ZonesController.cs @@ -0,0 +1,114 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Services; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ZonesController(MapEditorDbContext MapDb, LoggerController Logger) : ControllerBase +{ + [HttpPost] + [Route("")] + public async Task> Create([FromBody] ZoneCreateModel zone) + { + try + { + var map = await MapDb.Maps.FindAsync(zone.MapId); + if (map == null) return new(false, $"Không tồn tại map id = {zone.MapId}"); + + if (MapEditorHelper.CalculateQuadrilateralArea(zone.X1, zone.Y1, zone.X2, zone.Y2, zone.X3, zone.Y3, zone.X4, zone.Y4) < map.ZoneMinSquareDefault) + return new(false, "Kích thước Zone quá nhỏ"); + + var entity = await MapDb.Zones.AddAsync(new() + { + MapId = zone.MapId, + Type = zone.Type, + Name = "", + X1 = zone.X1, + X2 = zone.X2, + X3 = zone.X3, + X4 = zone.X4, + Y1 = zone.Y1, + Y2 = zone.Y2, + Y3 = zone.Y3, + Y4 = zone.Y4, + }); + await MapDb.SaveChangesAsync(); + return new(true) + { + Data = new ZoneDto() { Id = entity.Entity.Id, MapId = entity.Entity.MapId, Type = entity.Entity.Type, X1 = entity.Entity.X1, X2 = entity.Entity.X2, X3 = entity.Entity.X3, X4 = entity.Entity.X4, Y1 = entity.Entity.Y1, Y2 = entity.Entity.Y2, Y3 = entity.Entity.Y3, Y4 = entity.Entity.Y4 } + }; + } + catch (Exception ex) + { + Logger.Warning($"Create: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpDelete] + [Route("{id}")] + public async Task Delete(Guid id) + { + try + { + var zoneExisted = await MapDb.Zones.FindAsync(id); + if (zoneExisted is not null) + { + MapDb.Zones.Remove(zoneExisted); + await MapDb.SaveChangesAsync(); + return new(true); + } + return new(false, "Hệ thống không tìm thấy khu vực Zone này"); + } + catch (Exception ex) + { + Logger.Warning($"Delete {id}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpPut] + [Route("")] + public async Task Update([FromBody] ZoneUpdateModel zone) + { + try + { + var zoneExisted = await MapDb.Zones.FindAsync(zone.Id); + if (zoneExisted is not null) + { + if (zoneExisted.Name != zone.Name && !string.IsNullOrWhiteSpace(zone.Name) && await MapDb.Zones.AnyAsync(z => z.Name == zone.Name && z.MapId == zoneExisted.MapId)) + { + return new(false, $"Tên zone {zone.Name} đã tồn tại trong map"); + } + + zoneExisted.Type = zone.Type; + zoneExisted.Name = zone.Name; + zoneExisted.X1 = zone.X1; + zoneExisted.X2 = zone.X2; + zoneExisted.X3 = zone.X3; + zoneExisted.X4 = zone.X4; + zoneExisted.Y1 = zone.Y1; + zoneExisted.Y2 = zone.Y2; + zoneExisted.Y3 = zone.Y3; + zoneExisted.Y4 = zone.Y4; + MapDb.Zones.Update(zoneExisted); + await MapDb.SaveChangesAsync(); + return new(true); + } + return new(false, "Hệ thống không tìm thấy khu vực Zone này"); + } + catch (Exception ex) + { + Logger.Warning($"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Update: Hệ thống có lỗi xảy ra - {ex.Message}"); + } + } +} diff --git a/RobotNet.MapManager/Data/Action.cs b/RobotNet.MapManager/Data/Action.cs new file mode 100644 index 0000000..088e7c1 --- /dev/null +++ b/RobotNet.MapManager/Data/Action.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("Actions")] +[Index(nameof(MapId), nameof(Name), Name = "IX_Action_MapId_Name")] +public class Action +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + public string Name { get; set; } + + [Column("Content", TypeName = "nvarchar(max)")] + public string Content { get; set; } + + public Map Map { get; set; } +} diff --git a/RobotNet.MapManager/Data/Edge.cs b/RobotNet.MapManager/Data/Edge.cs new file mode 100644 index 0000000..3681396 --- /dev/null +++ b/RobotNet.MapManager/Data/Edge.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore; +using RobotNet.MapShares.Enums; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("Edges")] +[Index(nameof(MapId), Name = "IX_Edge_MapId")] +public class Edge +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("StartNodeId", TypeName = "uniqueidentifier")] + [Required] + public Guid StartNodeId { get; set; } + + [Column("EndNodeId", TypeName = "uniqueidentifier")] + [Required] + public Guid EndNodeId { get; set; } + + [Column("ControlPoint1X", TypeName = "float")] + public double ControlPoint1X { get; set; } + + [Column("ControlPoint1Y", TypeName = "float")] + public double ControlPoint1Y { get; set; } + + [Column("ControlPoint2X", TypeName = "float")] + public double ControlPoint2X { get; set; } + + [Column("ControlPoint2Y", TypeName = "float")] + public double ControlPoint2Y { get; set; } + + [Column("TrajectoryDegree", TypeName = "tinyint")] + public TrajectoryDegree TrajectoryDegree { get; set; } + + [Column("MaxHeight", TypeName = "float")] + public double MaxHeight { get; set; } + + [Column("MinHeight", TypeName = "float")] + public double MinHeight { get; set; } + + [Column("DirectionAllowed", TypeName = "tinyint")] + public DirectionAllowed DirectionAllowed { get; set; } + + [Column("RotationAllowed", TypeName = "bit")] + public bool RotationAllowed { get; set; } + + [Column("MaxRotationSpeed", TypeName = "float")] + public double MaxRotationSpeed { get; set; } + + [Column("MaxSpeed", TypeName = "float")] + public double MaxSpeed { get; set; } + + [Column("AllowedDeviationXy", TypeName = "float")] + public double AllowedDeviationXy { get; set; } + + [Column("AllowedDeviationTheta", TypeName = "float")] + public double AllowedDeviationTheta { get; set; } + + [Column("Actions", TypeName = "nvarchar(max)")] + public string Actions { get; set; } + + public Map Map { get; set; } + public virtual Node StartNode { get; set; } + public virtual Node EndNode { get; set; } +} diff --git a/RobotNet.MapManager/Data/Element.cs b/RobotNet.MapManager/Data/Element.cs new file mode 100644 index 0000000..112651d --- /dev/null +++ b/RobotNet.MapManager/Data/Element.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("Elements")] +[Index(nameof(MapId), nameof(ModelId), Name = "IX_Element_MapId_ModelId")] +public class Element +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("ModelId", TypeName = "uniqueidentifier")] + [Required] + public Guid ModelId { get; set; } + + [Column("NodeId", TypeName = "uniqueidentifier")] + [Required] + public Guid NodeId { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + public string Name { get; set; } + + [Column("IsOpen", TypeName = "bit")] + public bool IsOpen { get; set; } + + [Column("OffsetX", TypeName = "float")] + public double OffsetX { get; set; } + + [Column("OffsetY", TypeName = "float")] + public double OffsetY { get; set; } + + [Column("Content", TypeName = "nvarchar(max)")] + public string Content { get; set; } + + public Map Map { get; set; } + public ElementModel Model { get; set; } + public Node Node { get; set; } +} diff --git a/RobotNet.MapManager/Data/ElementModel.cs b/RobotNet.MapManager/Data/ElementModel.cs new file mode 100644 index 0000000..afaf399 --- /dev/null +++ b/RobotNet.MapManager/Data/ElementModel.cs @@ -0,0 +1,55 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("ElementModels")] +[Index(nameof(MapId), nameof(Name), Name = "IX_ElementModel_MapId_Name")] +public class ElementModel +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + public string Name { get; set; } + + [Column("Width", TypeName = "float")] + [Required] + public double Width { get; set; } + + [Column("Height", TypeName = "float")] + [Required] + public double Height { get; set; } + + [Column("Image1Width", TypeName = "int")] + [Required] + public int Image1Width { get; set; } + + [Column("Image1Height", TypeName = "int")] + [Required] + public int Image1Height { get; set; } + + [Column("Image2Width", TypeName = "int")] + [Required] + public int Image2Width { get; set; } + + [Column("Image2Height", TypeName = "int")] + [Required] + public int Image2Height { get; set; } + + [Column("Content", TypeName = "nvarchar(max)")] + public string Content { get; set; } + + public virtual ICollection Elements { get; } = []; + public Map Map { get; set; } +} diff --git a/RobotNet.MapManager/Data/Map.cs b/RobotNet.MapManager/Data/Map.cs new file mode 100644 index 0000000..63ab50b --- /dev/null +++ b/RobotNet.MapManager/Data/Map.cs @@ -0,0 +1,125 @@ +using RobotNet.MapShares.Enums; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("Maps")] +public class Map +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + [Required] + public string Name { get; set; } + + [Column("Description", TypeName = "ntext")] + public string Description { get; set; } = ""; + + [Column("VersionId", TypeName = "uniqueidentifier")] + [Required] + public Guid VersionId { get; set; } + + [Column("OriginX", TypeName = "float")] + [Required] + public double OriginX { get; set; } + + [Column("OriginY", TypeName = "float")] + [Required] + public double OriginY { get; set; } + + [Column("Resolution", TypeName = "float")] + [Required] + public double Resolution { get; set; } + + [Column("ViewX", TypeName = "float")] + [Required] + public double ViewX { get; set; } + + [Column("ViewY", TypeName = "float")] + [Required] + public double ViewY { get; set; } + + [Column("ViewWidth", TypeName = "float")] + [Required] + public double ViewWidth { get; set; } + + [Column("ViewHeight", TypeName = "float")] + [Required] + public double ViewHeight { get; set; } + + [Column("ImageWidth", TypeName = "float")] + [Required] + public double ImageWidth { get; set; } + + [Column("ImageHeight", TypeName = "float")] + [Required] + public double ImageHeight { get; set; } + + [Column("NodeCount", TypeName = "BigInt")] + public Int64 NodeCount { get; set; } + + [Column("Active", TypeName = "bit")] + public bool Active { get; set; } + + [Column("VDA5050", TypeName = "nvarchar(max)")] + public string VDA5050 { get; set; } = ""; //AdditionalAttributes + + [Column("NodeNameAutoGenerate", TypeName = "bit")] + public bool NodeNameAutoGenerate { get; set; } + + [Column("NodeNameTemplateDefault", TypeName = "nvarchar(64)")] + public string NodeNameTemplateDefault { get; set; } + + [Column("NodeAllowedDeviationXyDefault", TypeName = "float")] + public double NodeAllowedDeviationXyDefault { get; set; } + + [Column("NodeAllowedDeviationThetaDefault", TypeName = "float")] + public double NodeAllowedDeviationThetaDefault { get; set; } + + [Column("EdgeMinLengthDefault", TypeName = "float")] + public double EdgeMinLengthDefault { get; set; } + + [Column("EdgeStraightMaxSpeedDefault", TypeName = "float")] + public double EdgeStraightMaxSpeedDefault { get; set; } + + [Column("EdgeCurveMaxSpeedDefault", TypeName = "float")] + public double EdgeCurveMaxSpeedDefault { get; set; } + + [Column("EdgeMaxHeightDefault", TypeName = "float")] + public double EdgeMaxHeightDefault { get; set; } + + [Column("EdgeMinHeightDefault", TypeName = "float")] + public double EdgeMinHeightDefault { get; set; } + + [Column("EdgeMaxRoataionSpeedDefault", TypeName = "float")] + public double EdgeMaxRotationSpeedDefault { get; set; } + + [Column("EdgeDirectionAllowedDefault", TypeName = "tinyint")] + public DirectionAllowed EdgeDirectionAllowedDefault { get; set; } + + [Column("EdgeRotationAllowedDefault", TypeName = "bit")] + public bool EdgeRotationAllowedDefault { get; set; } + + [Column("EdgeAllowedDeviationXyDefault", TypeName = "float")] + public double EdgeAllowedDeviationXyDefault { get; set; } + + [Column("EdgeAllowedDeviationThetaDefault", TypeName = "float")] + public double EdgeAllowedDeviationThetaDefault { get; set; } + + [Column("ZoneMinSquareDefault", TypeName = "float")] + public double ZoneMinSquareDefault { get; set; } + + public virtual ICollection Nodes { get; } = []; + public virtual ICollection Edges { get; } = []; + public virtual ICollection Actions { get; } = []; + public virtual ICollection Zones { get; } = []; + public virtual ICollection ElementModels { get; } = []; + public virtual ICollection Elements { get; } = []; +} diff --git a/RobotNet.MapManager/Data/MapEditorDbContext.cs b/RobotNet.MapManager/Data/MapEditorDbContext.cs new file mode 100644 index 0000000..9e7b0c6 --- /dev/null +++ b/RobotNet.MapManager/Data/MapEditorDbContext.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.MapManager.Data; + + +public class MapEditorDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Maps { get; private set; } + public DbSet Nodes { get; private set; } + public DbSet Edges { get; private set; } + public DbSet Zones { get; private set; } + public DbSet Actions { get; private set; } + public DbSet ElementModels { get; private set; } + public DbSet Elements { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(e => e.Actions) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Edges) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Nodes) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Zones) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(e => e.StartNode) + .WithMany(n => n.StartEdges) + .HasForeignKey(e => e.StartNodeId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(e => e.EndNode) + .WithMany(n => n.EndEdges) + .HasForeignKey(e => e.EndNodeId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.ElementModels) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Elements) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasOne(n => n.Element) + .WithOne(e => e.Node) + .HasForeignKey(e => e.NodeId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Elements) + .WithOne(e => e.Model) + .HasForeignKey(e => e.ModelId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/RobotNet.MapManager/Data/MapManagerDbExtensions.cs b/RobotNet.MapManager/Data/MapManagerDbExtensions.cs new file mode 100644 index 0000000..f65e379 --- /dev/null +++ b/RobotNet.MapManager/Data/MapManagerDbExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.MapManager.Data; + +public static class MapManagerDbExtensions +{ + public static async Task SeedMapManagerDbAsync(this IServiceProvider serviceProvider) + { + using var scope = serviceProvider.GetRequiredService().CreateScope(); + + using var appDb = scope.ServiceProvider.GetRequiredService(); + + await appDb.Database.MigrateAsync(); + //await appDb.Database.EnsureCreatedAsync(); + await appDb.SaveChangesAsync(); + } +} diff --git a/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.Designer.cs b/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.Designer.cs new file mode 100644 index 0000000..8cc70fd --- /dev/null +++ b/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.Designer.cs @@ -0,0 +1,603 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.MapManager.Data; + +#nullable disable + +namespace RobotNet.MapManager.Data.Migrations +{ + [DbContext(typeof(MapEditorDbContext))] + [Migration("20250425030649_AddMapdb")] + partial class AddMapdb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Action_MapId_Name"); + + b.ToTable("Actions"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("ControlPoint1X") + .HasColumnType("float") + .HasColumnName("ControlPoint1X"); + + b.Property("ControlPoint1Y") + .HasColumnType("float") + .HasColumnName("ControlPoint1Y"); + + b.Property("ControlPoint2X") + .HasColumnType("float") + .HasColumnName("ControlPoint2X"); + + b.Property("ControlPoint2Y") + .HasColumnType("float") + .HasColumnName("ControlPoint2Y"); + + b.Property("DirectionAllowed") + .HasColumnType("tinyint") + .HasColumnName("DirectionAllowed"); + + b.Property("EndNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("EndNodeId"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("MaxHeight") + .HasColumnType("float") + .HasColumnName("MaxHeight"); + + b.Property("MaxRotationSpeed") + .HasColumnType("float") + .HasColumnName("MaxRotationSpeed"); + + b.Property("MaxSpeed") + .HasColumnType("float") + .HasColumnName("MaxSpeed"); + + b.Property("MinHeight") + .HasColumnType("float") + .HasColumnName("MinHeight"); + + b.Property("RotationAllowed") + .HasColumnType("bit") + .HasColumnName("RotationAllowed"); + + b.Property("StartNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("StartNodeId"); + + b.Property("TrajectoryDegree") + .HasColumnType("tinyint") + .HasColumnName("TrajectoryDegree"); + + b.HasKey("Id"); + + b.HasIndex("EndNodeId"); + + b.HasIndex("StartNodeId"); + + b.HasIndex(new[] { "MapId" }, "IX_Edge_MapId"); + + b.ToTable("Edges"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("IsOpen") + .HasColumnType("bit") + .HasColumnName("IsOpen"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("NodeId"); + + b.Property("OffsetX") + .HasColumnType("float") + .HasColumnName("OffsetX"); + + b.Property("OffsetY") + .HasColumnType("float") + .HasColumnName("OffsetY"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.HasIndex("NodeId") + .IsUnique(); + + b.HasIndex(new[] { "MapId", "ModelId" }, "IX_Element_MapId_ModelId"); + + b.ToTable("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("Height") + .HasColumnType("float") + .HasColumnName("Height"); + + b.Property("Image1Height") + .HasColumnType("int") + .HasColumnName("Image1Height"); + + b.Property("Image1Width") + .HasColumnType("int") + .HasColumnName("Image1Width"); + + b.Property("Image2Height") + .HasColumnType("int") + .HasColumnName("Image2Height"); + + b.Property("Image2Width") + .HasColumnType("int") + .HasColumnName("Image2Width"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_ElementModel_MapId_Name"); + + b.ToTable("ElementModels"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("bit") + .HasColumnName("Active"); + + b.Property("Description") + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("EdgeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationThetaDefault"); + + b.Property("EdgeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationXyDefault"); + + b.Property("EdgeCurveMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeCurveMaxSpeedDefault"); + + b.Property("EdgeDirectionAllowedDefault") + .HasColumnType("tinyint") + .HasColumnName("EdgeDirectionAllowedDefault"); + + b.Property("EdgeMaxHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxHeightDefault"); + + b.Property("EdgeMaxRotationSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxRoataionSpeedDefault"); + + b.Property("EdgeMinHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinHeightDefault"); + + b.Property("EdgeMinLengthDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinLengthDefault"); + + b.Property("EdgeRotationAllowedDefault") + .HasColumnType("bit") + .HasColumnName("EdgeRotationAllowedDefault"); + + b.Property("EdgeStraightMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeStraightMaxSpeedDefault"); + + b.Property("ImageHeight") + .HasColumnType("float") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("float") + .HasColumnName("ImageWidth"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationThetaDefault"); + + b.Property("NodeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationXyDefault"); + + b.Property("NodeCount") + .HasColumnType("BigInt") + .HasColumnName("NodeCount"); + + b.Property("NodeNameAutoGenerate") + .HasColumnType("bit") + .HasColumnName("NodeNameAutoGenerate"); + + b.Property("NodeNameTemplateDefault") + .HasColumnType("nvarchar(64)") + .HasColumnName("NodeNameTemplateDefault"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Resolution") + .HasColumnType("float") + .HasColumnName("Resolution"); + + b.Property("VDA5050") + .HasColumnType("nvarchar(max)") + .HasColumnName("VDA5050"); + + b.Property("VersionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("VersionId"); + + b.Property("ViewHeight") + .HasColumnType("float") + .HasColumnName("ViewHeight"); + + b.Property("ViewWidth") + .HasColumnType("float") + .HasColumnName("ViewWidth"); + + b.Property("ViewX") + .HasColumnType("float") + .HasColumnName("ViewX"); + + b.Property("ViewY") + .HasColumnType("float") + .HasColumnName("ViewY"); + + b.Property("ZoneMinSquareDefault") + .HasColumnType("float") + .HasColumnName("ZoneMinSquareDefault"); + + b.HasKey("Id"); + + b.ToTable("Maps"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Theta") + .HasColumnType("float") + .HasColumnName("Theta"); + + b.Property("X") + .HasColumnType("float") + .HasColumnName("X"); + + b.Property("Y") + .HasColumnType("float") + .HasColumnName("Y"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Node_MapId_Name"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("Type"); + + b.Property("X1") + .HasColumnType("float") + .HasColumnName("X1"); + + b.Property("X2") + .HasColumnType("float") + .HasColumnName("X2"); + + b.Property("X3") + .HasColumnType("float") + .HasColumnName("X3"); + + b.Property("X4") + .HasColumnType("float") + .HasColumnName("X4"); + + b.Property("Y1") + .HasColumnType("float") + .HasColumnName("Y1"); + + b.Property("Y2") + .HasColumnType("float") + .HasColumnName("Y2"); + + b.Property("Y3") + .HasColumnType("float") + .HasColumnName("Y3"); + + b.Property("Y4") + .HasColumnType("float") + .HasColumnName("Y4"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId" }, "IX_Zone_MapId"); + + b.ToTable("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Actions") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.HasOne("RobotNet.MapManager.Data.Node", "EndNode") + .WithMany("EndEdges") + .HasForeignKey("EndNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Edges") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "StartNode") + .WithMany("StartEdges") + .HasForeignKey("StartNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EndNode"); + + b.Navigation("Map"); + + b.Navigation("StartNode"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Elements") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.ElementModel", "Model") + .WithMany("Elements") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "Node") + .WithOne("Element") + .HasForeignKey("RobotNet.MapManager.Data.Element", "NodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Model"); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("ElementModels") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Nodes") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Zones") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Navigation("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Navigation("Actions"); + + b.Navigation("Edges"); + + b.Navigation("ElementModels"); + + b.Navigation("Elements"); + + b.Navigation("Nodes"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Navigation("Element"); + + b.Navigation("EndEdges"); + + b.Navigation("StartEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.cs b/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.cs new file mode 100644 index 0000000..84a50e7 --- /dev/null +++ b/RobotNet.MapManager/Data/Migrations/20250425030649_AddMapdb.cs @@ -0,0 +1,313 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.MapManager.Data.Migrations +{ + /// + public partial class AddMapdb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Maps", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(64)", nullable: false), + Description = table.Column(type: "ntext", nullable: true), + VersionId = table.Column(type: "uniqueidentifier", nullable: false), + OriginX = table.Column(type: "float", nullable: false), + OriginY = table.Column(type: "float", nullable: false), + Resolution = table.Column(type: "float", nullable: false), + ViewX = table.Column(type: "float", nullable: false), + ViewY = table.Column(type: "float", nullable: false), + ViewWidth = table.Column(type: "float", nullable: false), + ViewHeight = table.Column(type: "float", nullable: false), + ImageWidth = table.Column(type: "float", nullable: false), + ImageHeight = table.Column(type: "float", nullable: false), + NodeCount = table.Column(type: "BigInt", nullable: false), + Active = table.Column(type: "bit", nullable: false), + VDA5050 = table.Column(type: "nvarchar(max)", nullable: true), + NodeNameAutoGenerate = table.Column(type: "bit", nullable: false), + NodeNameTemplateDefault = table.Column(type: "nvarchar(64)", nullable: true), + NodeAllowedDeviationXyDefault = table.Column(type: "float", nullable: false), + NodeAllowedDeviationThetaDefault = table.Column(type: "float", nullable: false), + EdgeMinLengthDefault = table.Column(type: "float", nullable: false), + EdgeStraightMaxSpeedDefault = table.Column(type: "float", nullable: false), + EdgeCurveMaxSpeedDefault = table.Column(type: "float", nullable: false), + EdgeMaxHeightDefault = table.Column(type: "float", nullable: false), + EdgeMinHeightDefault = table.Column(type: "float", nullable: false), + EdgeMaxRoataionSpeedDefault = table.Column(type: "float", nullable: false), + EdgeDirectionAllowedDefault = table.Column(type: "tinyint", nullable: false), + EdgeRotationAllowedDefault = table.Column(type: "bit", nullable: false), + EdgeAllowedDeviationXyDefault = table.Column(type: "float", nullable: false), + EdgeAllowedDeviationThetaDefault = table.Column(type: "float", nullable: false), + ZoneMinSquareDefault = table.Column(type: "float", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Maps", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Actions", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(64)", nullable: true), + Content = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Actions", x => x.Id); + table.ForeignKey( + name: "FK_Actions_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ElementModels", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(64)", nullable: true), + Width = table.Column(type: "float", nullable: false), + Height = table.Column(type: "float", nullable: false), + Image1Width = table.Column(type: "int", nullable: false), + Image1Height = table.Column(type: "int", nullable: false), + Image2Width = table.Column(type: "int", nullable: false), + Image2Height = table.Column(type: "int", nullable: false), + Content = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ElementModels", x => x.Id); + table.ForeignKey( + name: "FK_ElementModels_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Nodes", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(64)", nullable: true), + X = table.Column(type: "float", nullable: false), + Y = table.Column(type: "float", nullable: false), + Theta = table.Column(type: "float", nullable: false), + AllowedDeviationXy = table.Column(type: "float", nullable: false), + AllowedDeviationTheta = table.Column(type: "float", nullable: false), + Actions = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Nodes", x => x.Id); + table.ForeignKey( + name: "FK_Nodes_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Zones", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + Type = table.Column(type: "int", nullable: false), + X1 = table.Column(type: "float", nullable: false), + Y1 = table.Column(type: "float", nullable: false), + X2 = table.Column(type: "float", nullable: false), + Y2 = table.Column(type: "float", nullable: false), + X3 = table.Column(type: "float", nullable: false), + Y3 = table.Column(type: "float", nullable: false), + X4 = table.Column(type: "float", nullable: false), + Y4 = table.Column(type: "float", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Zones", x => x.Id); + table.ForeignKey( + name: "FK_Zones_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Edges", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + StartNodeId = table.Column(type: "uniqueidentifier", nullable: false), + EndNodeId = table.Column(type: "uniqueidentifier", nullable: false), + ControlPoint1X = table.Column(type: "float", nullable: false), + ControlPoint1Y = table.Column(type: "float", nullable: false), + ControlPoint2X = table.Column(type: "float", nullable: false), + ControlPoint2Y = table.Column(type: "float", nullable: false), + TrajectoryDegree = table.Column(type: "tinyint", nullable: false), + MaxHeight = table.Column(type: "float", nullable: false), + MinHeight = table.Column(type: "float", nullable: false), + DirectionAllowed = table.Column(type: "tinyint", nullable: false), + RotationAllowed = table.Column(type: "bit", nullable: false), + MaxRotationSpeed = table.Column(type: "float", nullable: false), + MaxSpeed = table.Column(type: "float", nullable: false), + AllowedDeviationXy = table.Column(type: "float", nullable: false), + AllowedDeviationTheta = table.Column(type: "float", nullable: false), + Actions = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Edges", x => x.Id); + table.ForeignKey( + name: "FK_Edges_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Edges_Nodes_EndNodeId", + column: x => x.EndNodeId, + principalTable: "Nodes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Edges_Nodes_StartNodeId", + column: x => x.StartNodeId, + principalTable: "Nodes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Elements", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + ModelId = table.Column(type: "uniqueidentifier", nullable: false), + NodeId = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(64)", nullable: true), + IsOpen = table.Column(type: "bit", nullable: false), + OffsetX = table.Column(type: "float", nullable: false), + OffsetY = table.Column(type: "float", nullable: false), + Content = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Elements", x => x.Id); + table.ForeignKey( + name: "FK_Elements_ElementModels_ModelId", + column: x => x.ModelId, + principalTable: "ElementModels", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Elements_Maps_MapId", + column: x => x.MapId, + principalTable: "Maps", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_Elements_Nodes_NodeId", + column: x => x.NodeId, + principalTable: "Nodes", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Action_MapId_Name", + table: "Actions", + columns: new[] { "MapId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_Edge_MapId", + table: "Edges", + column: "MapId"); + + migrationBuilder.CreateIndex( + name: "IX_Edges_EndNodeId", + table: "Edges", + column: "EndNodeId"); + + migrationBuilder.CreateIndex( + name: "IX_Edges_StartNodeId", + table: "Edges", + column: "StartNodeId"); + + migrationBuilder.CreateIndex( + name: "IX_ElementModel_MapId_Name", + table: "ElementModels", + columns: new[] { "MapId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_Element_MapId_ModelId", + table: "Elements", + columns: new[] { "MapId", "ModelId" }); + + migrationBuilder.CreateIndex( + name: "IX_Elements_ModelId", + table: "Elements", + column: "ModelId"); + + migrationBuilder.CreateIndex( + name: "IX_Elements_NodeId", + table: "Elements", + column: "NodeId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Node_MapId_Name", + table: "Nodes", + columns: new[] { "MapId", "Name" }); + + migrationBuilder.CreateIndex( + name: "IX_Zone_MapId", + table: "Zones", + column: "MapId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Actions"); + + migrationBuilder.DropTable( + name: "Edges"); + + migrationBuilder.DropTable( + name: "Elements"); + + migrationBuilder.DropTable( + name: "Zones"); + + migrationBuilder.DropTable( + name: "ElementModels"); + + migrationBuilder.DropTable( + name: "Nodes"); + + migrationBuilder.DropTable( + name: "Maps"); + } + } +} diff --git a/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.Designer.cs b/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.Designer.cs new file mode 100644 index 0000000..bf5777d --- /dev/null +++ b/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.Designer.cs @@ -0,0 +1,608 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.MapManager.Data; + +#nullable disable + +namespace RobotNet.MapManager.Data.Migrations +{ + [DbContext(typeof(MapEditorDbContext))] + [Migration("20250812041834_AddZoneName")] + partial class AddZoneName + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Action_MapId_Name"); + + b.ToTable("Actions"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("ControlPoint1X") + .HasColumnType("float") + .HasColumnName("ControlPoint1X"); + + b.Property("ControlPoint1Y") + .HasColumnType("float") + .HasColumnName("ControlPoint1Y"); + + b.Property("ControlPoint2X") + .HasColumnType("float") + .HasColumnName("ControlPoint2X"); + + b.Property("ControlPoint2Y") + .HasColumnType("float") + .HasColumnName("ControlPoint2Y"); + + b.Property("DirectionAllowed") + .HasColumnType("tinyint") + .HasColumnName("DirectionAllowed"); + + b.Property("EndNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("EndNodeId"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("MaxHeight") + .HasColumnType("float") + .HasColumnName("MaxHeight"); + + b.Property("MaxRotationSpeed") + .HasColumnType("float") + .HasColumnName("MaxRotationSpeed"); + + b.Property("MaxSpeed") + .HasColumnType("float") + .HasColumnName("MaxSpeed"); + + b.Property("MinHeight") + .HasColumnType("float") + .HasColumnName("MinHeight"); + + b.Property("RotationAllowed") + .HasColumnType("bit") + .HasColumnName("RotationAllowed"); + + b.Property("StartNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("StartNodeId"); + + b.Property("TrajectoryDegree") + .HasColumnType("tinyint") + .HasColumnName("TrajectoryDegree"); + + b.HasKey("Id"); + + b.HasIndex("EndNodeId"); + + b.HasIndex("StartNodeId"); + + b.HasIndex(new[] { "MapId" }, "IX_Edge_MapId"); + + b.ToTable("Edges"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("IsOpen") + .HasColumnType("bit") + .HasColumnName("IsOpen"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("NodeId"); + + b.Property("OffsetX") + .HasColumnType("float") + .HasColumnName("OffsetX"); + + b.Property("OffsetY") + .HasColumnType("float") + .HasColumnName("OffsetY"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.HasIndex("NodeId") + .IsUnique(); + + b.HasIndex(new[] { "MapId", "ModelId" }, "IX_Element_MapId_ModelId"); + + b.ToTable("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("Height") + .HasColumnType("float") + .HasColumnName("Height"); + + b.Property("Image1Height") + .HasColumnType("int") + .HasColumnName("Image1Height"); + + b.Property("Image1Width") + .HasColumnType("int") + .HasColumnName("Image1Width"); + + b.Property("Image2Height") + .HasColumnType("int") + .HasColumnName("Image2Height"); + + b.Property("Image2Width") + .HasColumnType("int") + .HasColumnName("Image2Width"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_ElementModel_MapId_Name"); + + b.ToTable("ElementModels"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("bit") + .HasColumnName("Active"); + + b.Property("Description") + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("EdgeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationThetaDefault"); + + b.Property("EdgeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationXyDefault"); + + b.Property("EdgeCurveMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeCurveMaxSpeedDefault"); + + b.Property("EdgeDirectionAllowedDefault") + .HasColumnType("tinyint") + .HasColumnName("EdgeDirectionAllowedDefault"); + + b.Property("EdgeMaxHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxHeightDefault"); + + b.Property("EdgeMaxRotationSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxRoataionSpeedDefault"); + + b.Property("EdgeMinHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinHeightDefault"); + + b.Property("EdgeMinLengthDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinLengthDefault"); + + b.Property("EdgeRotationAllowedDefault") + .HasColumnType("bit") + .HasColumnName("EdgeRotationAllowedDefault"); + + b.Property("EdgeStraightMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeStraightMaxSpeedDefault"); + + b.Property("ImageHeight") + .HasColumnType("float") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("float") + .HasColumnName("ImageWidth"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationThetaDefault"); + + b.Property("NodeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationXyDefault"); + + b.Property("NodeCount") + .HasColumnType("BigInt") + .HasColumnName("NodeCount"); + + b.Property("NodeNameAutoGenerate") + .HasColumnType("bit") + .HasColumnName("NodeNameAutoGenerate"); + + b.Property("NodeNameTemplateDefault") + .HasColumnType("nvarchar(64)") + .HasColumnName("NodeNameTemplateDefault"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Resolution") + .HasColumnType("float") + .HasColumnName("Resolution"); + + b.Property("VDA5050") + .HasColumnType("nvarchar(max)") + .HasColumnName("VDA5050"); + + b.Property("VersionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("VersionId"); + + b.Property("ViewHeight") + .HasColumnType("float") + .HasColumnName("ViewHeight"); + + b.Property("ViewWidth") + .HasColumnType("float") + .HasColumnName("ViewWidth"); + + b.Property("ViewX") + .HasColumnType("float") + .HasColumnName("ViewX"); + + b.Property("ViewY") + .HasColumnType("float") + .HasColumnName("ViewY"); + + b.Property("ZoneMinSquareDefault") + .HasColumnType("float") + .HasColumnName("ZoneMinSquareDefault"); + + b.HasKey("Id"); + + b.ToTable("Maps"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Theta") + .HasColumnType("float") + .HasColumnName("Theta"); + + b.Property("X") + .HasColumnType("float") + .HasColumnName("X"); + + b.Property("Y") + .HasColumnType("float") + .HasColumnName("Y"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Node_MapId_Name"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("Type"); + + b.Property("X1") + .HasColumnType("float") + .HasColumnName("X1"); + + b.Property("X2") + .HasColumnType("float") + .HasColumnName("X2"); + + b.Property("X3") + .HasColumnType("float") + .HasColumnName("X3"); + + b.Property("X4") + .HasColumnType("float") + .HasColumnName("X4"); + + b.Property("Y1") + .HasColumnType("float") + .HasColumnName("Y1"); + + b.Property("Y2") + .HasColumnType("float") + .HasColumnName("Y2"); + + b.Property("Y3") + .HasColumnType("float") + .HasColumnName("Y3"); + + b.Property("Y4") + .HasColumnType("float") + .HasColumnName("Y4"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId" }, "IX_Zone_MapId"); + + b.ToTable("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Actions") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.HasOne("RobotNet.MapManager.Data.Node", "EndNode") + .WithMany("EndEdges") + .HasForeignKey("EndNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Edges") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "StartNode") + .WithMany("StartEdges") + .HasForeignKey("StartNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EndNode"); + + b.Navigation("Map"); + + b.Navigation("StartNode"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Elements") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.ElementModel", "Model") + .WithMany("Elements") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "Node") + .WithOne("Element") + .HasForeignKey("RobotNet.MapManager.Data.Element", "NodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Model"); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("ElementModels") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Nodes") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Zones") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Navigation("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Navigation("Actions"); + + b.Navigation("Edges"); + + b.Navigation("ElementModels"); + + b.Navigation("Elements"); + + b.Navigation("Nodes"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Navigation("Element"); + + b.Navigation("EndEdges"); + + b.Navigation("StartEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.cs b/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.cs new file mode 100644 index 0000000..e10219d --- /dev/null +++ b/RobotNet.MapManager/Data/Migrations/20250812041834_AddZoneName.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.MapManager.Data.Migrations +{ + /// + public partial class AddZoneName : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Name", + table: "Zones", + type: "nvarchar(64)", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "Zones"); + } + } +} diff --git a/RobotNet.MapManager/Data/Migrations/MapEditorDbContextModelSnapshot.cs b/RobotNet.MapManager/Data/Migrations/MapEditorDbContextModelSnapshot.cs new file mode 100644 index 0000000..1346875 --- /dev/null +++ b/RobotNet.MapManager/Data/Migrations/MapEditorDbContextModelSnapshot.cs @@ -0,0 +1,605 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.MapManager.Data; + +#nullable disable + +namespace RobotNet.MapManager.Data.Migrations +{ + [DbContext(typeof(MapEditorDbContext))] + partial class MapEditorDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Action_MapId_Name"); + + b.ToTable("Actions"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("ControlPoint1X") + .HasColumnType("float") + .HasColumnName("ControlPoint1X"); + + b.Property("ControlPoint1Y") + .HasColumnType("float") + .HasColumnName("ControlPoint1Y"); + + b.Property("ControlPoint2X") + .HasColumnType("float") + .HasColumnName("ControlPoint2X"); + + b.Property("ControlPoint2Y") + .HasColumnType("float") + .HasColumnName("ControlPoint2Y"); + + b.Property("DirectionAllowed") + .HasColumnType("tinyint") + .HasColumnName("DirectionAllowed"); + + b.Property("EndNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("EndNodeId"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("MaxHeight") + .HasColumnType("float") + .HasColumnName("MaxHeight"); + + b.Property("MaxRotationSpeed") + .HasColumnType("float") + .HasColumnName("MaxRotationSpeed"); + + b.Property("MaxSpeed") + .HasColumnType("float") + .HasColumnName("MaxSpeed"); + + b.Property("MinHeight") + .HasColumnType("float") + .HasColumnName("MinHeight"); + + b.Property("RotationAllowed") + .HasColumnType("bit") + .HasColumnName("RotationAllowed"); + + b.Property("StartNodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("StartNodeId"); + + b.Property("TrajectoryDegree") + .HasColumnType("tinyint") + .HasColumnName("TrajectoryDegree"); + + b.HasKey("Id"); + + b.HasIndex("EndNodeId"); + + b.HasIndex("StartNodeId"); + + b.HasIndex(new[] { "MapId" }, "IX_Edge_MapId"); + + b.ToTable("Edges"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("IsOpen") + .HasColumnType("bit") + .HasColumnName("IsOpen"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeId") + .HasColumnType("uniqueidentifier") + .HasColumnName("NodeId"); + + b.Property("OffsetX") + .HasColumnType("float") + .HasColumnName("OffsetX"); + + b.Property("OffsetY") + .HasColumnType("float") + .HasColumnName("OffsetY"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.HasIndex("NodeId") + .IsUnique(); + + b.HasIndex(new[] { "MapId", "ModelId" }, "IX_Element_MapId_ModelId"); + + b.ToTable("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Content") + .HasColumnType("nvarchar(max)") + .HasColumnName("Content"); + + b.Property("Height") + .HasColumnType("float") + .HasColumnName("Height"); + + b.Property("Image1Height") + .HasColumnType("int") + .HasColumnName("Image1Height"); + + b.Property("Image1Width") + .HasColumnType("int") + .HasColumnName("Image1Width"); + + b.Property("Image2Height") + .HasColumnType("int") + .HasColumnName("Image2Height"); + + b.Property("Image2Width") + .HasColumnType("int") + .HasColumnName("Image2Width"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_ElementModel_MapId_Name"); + + b.ToTable("ElementModels"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Active") + .HasColumnType("bit") + .HasColumnName("Active"); + + b.Property("Description") + .HasColumnType("ntext") + .HasColumnName("Description"); + + b.Property("EdgeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationThetaDefault"); + + b.Property("EdgeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("EdgeAllowedDeviationXyDefault"); + + b.Property("EdgeCurveMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeCurveMaxSpeedDefault"); + + b.Property("EdgeDirectionAllowedDefault") + .HasColumnType("tinyint") + .HasColumnName("EdgeDirectionAllowedDefault"); + + b.Property("EdgeMaxHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxHeightDefault"); + + b.Property("EdgeMaxRotationSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeMaxRoataionSpeedDefault"); + + b.Property("EdgeMinHeightDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinHeightDefault"); + + b.Property("EdgeMinLengthDefault") + .HasColumnType("float") + .HasColumnName("EdgeMinLengthDefault"); + + b.Property("EdgeRotationAllowedDefault") + .HasColumnType("bit") + .HasColumnName("EdgeRotationAllowedDefault"); + + b.Property("EdgeStraightMaxSpeedDefault") + .HasColumnType("float") + .HasColumnName("EdgeStraightMaxSpeedDefault"); + + b.Property("ImageHeight") + .HasColumnType("float") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("float") + .HasColumnName("ImageWidth"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("NodeAllowedDeviationThetaDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationThetaDefault"); + + b.Property("NodeAllowedDeviationXyDefault") + .HasColumnType("float") + .HasColumnName("NodeAllowedDeviationXyDefault"); + + b.Property("NodeCount") + .HasColumnType("BigInt") + .HasColumnName("NodeCount"); + + b.Property("NodeNameAutoGenerate") + .HasColumnType("bit") + .HasColumnName("NodeNameAutoGenerate"); + + b.Property("NodeNameTemplateDefault") + .HasColumnType("nvarchar(64)") + .HasColumnName("NodeNameTemplateDefault"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Resolution") + .HasColumnType("float") + .HasColumnName("Resolution"); + + b.Property("VDA5050") + .HasColumnType("nvarchar(max)") + .HasColumnName("VDA5050"); + + b.Property("VersionId") + .HasColumnType("uniqueidentifier") + .HasColumnName("VersionId"); + + b.Property("ViewHeight") + .HasColumnType("float") + .HasColumnName("ViewHeight"); + + b.Property("ViewWidth") + .HasColumnType("float") + .HasColumnName("ViewWidth"); + + b.Property("ViewX") + .HasColumnType("float") + .HasColumnName("ViewX"); + + b.Property("ViewY") + .HasColumnType("float") + .HasColumnName("ViewY"); + + b.Property("ZoneMinSquareDefault") + .HasColumnType("float") + .HasColumnName("ZoneMinSquareDefault"); + + b.HasKey("Id"); + + b.ToTable("Maps"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("Actions") + .HasColumnType("nvarchar(max)") + .HasColumnName("Actions"); + + b.Property("AllowedDeviationTheta") + .HasColumnType("float") + .HasColumnName("AllowedDeviationTheta"); + + b.Property("AllowedDeviationXy") + .HasColumnType("float") + .HasColumnName("AllowedDeviationXy"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Theta") + .HasColumnType("float") + .HasColumnName("Theta"); + + b.Property("X") + .HasColumnType("float") + .HasColumnName("X"); + + b.Property("Y") + .HasColumnType("float") + .HasColumnName("Y"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId", "Name" }, "IX_Node_MapId_Name"); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasColumnName("Name"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("Type"); + + b.Property("X1") + .HasColumnType("float") + .HasColumnName("X1"); + + b.Property("X2") + .HasColumnType("float") + .HasColumnName("X2"); + + b.Property("X3") + .HasColumnType("float") + .HasColumnName("X3"); + + b.Property("X4") + .HasColumnType("float") + .HasColumnName("X4"); + + b.Property("Y1") + .HasColumnType("float") + .HasColumnName("Y1"); + + b.Property("Y2") + .HasColumnType("float") + .HasColumnName("Y2"); + + b.Property("Y3") + .HasColumnType("float") + .HasColumnName("Y3"); + + b.Property("Y4") + .HasColumnType("float") + .HasColumnName("Y4"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "MapId" }, "IX_Zone_MapId"); + + b.ToTable("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Action", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Actions") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Edge", b => + { + b.HasOne("RobotNet.MapManager.Data.Node", "EndNode") + .WithMany("EndEdges") + .HasForeignKey("EndNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Edges") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "StartNode") + .WithMany("StartEdges") + .HasForeignKey("StartNodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EndNode"); + + b.Navigation("Map"); + + b.Navigation("StartNode"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Element", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Elements") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.ElementModel", "Model") + .WithMany("Elements") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("RobotNet.MapManager.Data.Node", "Node") + .WithOne("Element") + .HasForeignKey("RobotNet.MapManager.Data.Element", "NodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + + b.Navigation("Model"); + + b.Navigation("Node"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("ElementModels") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Nodes") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Zone", b => + { + b.HasOne("RobotNet.MapManager.Data.Map", "Map") + .WithMany("Zones") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Map"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.ElementModel", b => + { + b.Navigation("Elements"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Map", b => + { + b.Navigation("Actions"); + + b.Navigation("Edges"); + + b.Navigation("ElementModels"); + + b.Navigation("Elements"); + + b.Navigation("Nodes"); + + b.Navigation("Zones"); + }); + + modelBuilder.Entity("RobotNet.MapManager.Data.Node", b => + { + b.Navigation("Element"); + + b.Navigation("EndEdges"); + + b.Navigation("StartEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.MapManager/Data/Node.cs b/RobotNet.MapManager/Data/Node.cs new file mode 100644 index 0000000..327ce3f --- /dev/null +++ b/RobotNet.MapManager/Data/Node.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapManager.Data; + +#nullable disable + +[Table("Nodes")] +[Index(nameof(MapId), nameof(Name), Name = "IX_Node_MapId_Name")] +public class Node +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + public string Name { get; set; } + + [Column("X", TypeName = "float")] + [Required] + public double X { get; set; } + + [Column("Y", TypeName = "float")] + [Required] + public double Y { get; set; } + + [Column("Theta", TypeName = "float")] + [Required] + public double Theta { get; set; } + + [Column("AllowedDeviationXy", TypeName = "float")] + public double AllowedDeviationXy { get; set; } + + [Column("AllowedDeviationTheta", TypeName = "float")] + public double AllowedDeviationTheta { get; set; } + + [Column("Actions", TypeName = "nvarchar(max)")] + public string Actions { get; set; } + + public Map Map { get; set; } + public Element Element { get; set; } + public ICollection StartEdges { get; } = []; + public ICollection EndEdges { get; } = []; +} diff --git a/RobotNet.MapManager/Data/Zone.cs b/RobotNet.MapManager/Data/Zone.cs new file mode 100644 index 0000000..16feb30 --- /dev/null +++ b/RobotNet.MapManager/Data/Zone.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapManager.Data; + +[Table("Zones")] +[Index(nameof(MapId), Name = "IX_Zone_MapId")] +public class Zone +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("Type", TypeName = "int")] + [Required] + public ZoneType Type { get; set; } + + [Column("Name", TypeName = "nvarchar(64)")] + [Required] + public string Name { get; set; } = ""; + + [Column("X1", TypeName = "float")] + [Required] + public double X1 { get; set; } + + [Column("Y1", TypeName = "float")] + [Required] + public double Y1 { get; set; } + + [Column("X2", TypeName = "float")] + [Required] + public double X2 { get; set; } + + [Column("Y2", TypeName = "float")] + [Required] + public double Y2 { get; set; } + + [Column("X3", TypeName = "float")] + [Required] + public double X3 { get; set; } + + [Column("Y3", TypeName = "float")] + [Required] + public double Y3 { get; set; } + + [Column("X4", TypeName = "float")] + public double X4 { get; set; } + + [Column("Y4", TypeName = "float")] + public double Y4 { get; set; } + + public Map Map { get; set; } = default!; +} diff --git a/RobotNet.MapManager/Dockerfile b/RobotNet.MapManager/Dockerfile new file mode 100644 index 0000000..fec74bb --- /dev/null +++ b/RobotNet.MapManager/Dockerfile @@ -0,0 +1,65 @@ +FROM alpine:3.22 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["RobotNet.MapManager/RobotNet.MapManager.csproj", "RobotNet.MapManager/"] +COPY ["RobotNet.MapShares/RobotNet.MapShares.csproj", "RobotNet.MapShares/"] +COPY ["RobotNet.Script/RobotNet.Script.csproj", "RobotNet.Script/"] +COPY ["RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj", "RobotNet.Script.Expressions/"] +COPY ["RobotNet.Shares/RobotNet.Shares.csproj", "RobotNet.Shares/"] +COPY ["RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj", "RobotNet.OpenIddictClient/"] + +# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.MapManager/RobotNet.MapManager.csproj" +RUN dotnet restore "RobotNet.MapManager/RobotNet.MapManager.csproj" + +COPY RobotNet.MapManager/ RobotNet.MapManager/ +COPY RobotNet.MapShares/ RobotNet.MapShares/ +COPY RobotNet.Script/ RobotNet.Script/ +COPY RobotNet.Script.Expressions/ RobotNet.Script.Expressions/ +COPY RobotNet.Shares/ RobotNet.Shares/ +COPY RobotNet.OpenIddictClient/ RobotNet.OpenIddictClient/ + +RUN rm -rf ./RobotNet.MapManager/bin +RUN rm -rf ./RobotNet.MapManager/obj +RUN rm -rf ./RobotNet.MapShares/bin +RUN rm -rf ./RobotNet.MapShares/obj +RUN rm -rf ./RobotNet.Script/bin +RUN rm -rf ./RobotNet.Script/obj +RUN rm -rf ./RobotNet.Script.Expressions/bin +RUN rm -rf ./RobotNet.Script.Expressions/obj +RUN rm -rf ./RobotNet.Shares/bin +RUN rm -rf ./RobotNet.Shares/obj +RUN rm -rf ./RobotNet.OpenIddictClient/bin +RUN rm -rf ./RobotNet.OpenIddictClient/obj + +WORKDIR "/src/RobotNet.MapManager" +RUN dotnet build "RobotNet.MapManager.csproj" -c Release -o /app/build + +FROM build AS publish +WORKDIR /src/RobotNet.MapManager +RUN dotnet publish "RobotNet.MapManager.csproj" \ + -c Release \ + -o /app/publish \ + --runtime linux-musl-x64 \ + --self-contained true \ + /p:PublishTrimmed=false \ + /p:PublishReadyToRun=true + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish ./ + +RUN apk add --no-cache icu-libs tzdata ca-certificates + +RUN echo '#!/bin/sh' >> ./start.sh +RUN echo 'update-ca-certificates' >> ./start.sh +RUN echo 'exec ./RobotNet.MapManager' >> ./start.sh + +RUN chmod +x ./RobotNet.MapManager +RUN chmod +x ./start.sh + +# Use the start script to ensure certificates are updated before starting the application +EXPOSE 443 +ENTRYPOINT ["./start.sh"] diff --git a/RobotNet.MapManager/Hubs/MapHub.cs b/RobotNet.MapManager/Hubs/MapHub.cs new file mode 100644 index 0000000..d7bafe0 --- /dev/null +++ b/RobotNet.MapManager/Hubs/MapHub.cs @@ -0,0 +1,165 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapManager.Data; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.MapManager.Hubs; + +[Authorize] +public class MapHub(MapEditorDbContext MapDb) : Hub +{ + public async Task> GetMapData(Guid mapId) + { + var map = await MapDb.Maps.FindAsync(mapId); + if (map is null) return new(false, $"Không tìm thấy bản đồ: {mapId}"); + + return new(true) + { + Data = new() + { + Id = map.Id, + Name = map.Name, + OriginX = map.OriginX, + OriginY = map.OriginY, + Resolution = map.Resolution, + ImageHeight = map.ImageHeight, + ImageWidth = map.ImageWidth, + Active = map.Active, + Nodes = [.. MapDb.Nodes.Where(node => node.MapId == mapId).Select(node => new NodeDto() + { + Id = node.Id, + MapId = node.MapId, + Name = node.Name, + Theta = node.Theta, + X = node.X, + Y = node.Y, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions, + })], + Edges = [.. MapDb.Edges.Where(edge => edge.MapId == mapId).Select(edge => new EdgeDto() + { + Id = edge.Id, + MapId = edge.MapId, + StartNodeId = edge.StartNodeId, + EndNodeId = edge.EndNodeId, + + MaxSpeed = edge.MaxSpeed, + MaxHeight = edge.MaxHeight, + MinHeight = edge.MinHeight, + DirectionAllowed = edge.DirectionAllowed, + RotationAllowed = edge.RotationAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + MaxRotationSpeed = edge.MaxRotationSpeed, + Actions = edge.Actions, + AllowedDeviationXy = edge.AllowedDeviationXy, + AllowedDeviationTheta = edge.AllowedDeviationTheta, + })], + Zones = [.. MapDb.Zones.Where(zone => zone.MapId == mapId).Select(zone => new ZoneDto() + { + Id = zone.Id, + MapId = zone.MapId, + Type = zone.Type, + Name = zone.Name, + X1 = zone.X1, + X2 = zone.X2, + Y1 = zone.Y1, + Y2 = zone.Y2, + X3 = zone.X3, + Y3 = zone.Y3, + X4 = zone.X4, + Y4 = zone.Y4, + }).OrderBy(z => z.Type)], + Elements = [.. MapDb.Elements.Where(el => el.MapId == mapId).Select(element => new ElementDto() + { + Id = element.Id, + MapId = element.MapId, + ModelId = element.ModelId, + Name = element.Name, + NodeId = element.NodeId, + OffsetX = element.OffsetX, + OffsetY = element.OffsetY, + IsOpen = element.IsOpen, + Content = element.Content, + })], + Actions = [.. MapDb.Actions.Where(a => a.MapId == mapId).Select(action => new ActionDto() + { + Id = action.Id, + MapId = action.MapId, + Name = action.Name, + Content = action.Content, + })] + } + }; + } + + public async Task> GetElementsState(Guid mapId) + { + var map = await MapDb.Maps.FindAsync(mapId); + if (map is null) return new(false, $"Không tìm thấy bản đồ: {mapId}"); + var elements = MapDb.Elements + .Where(el => el.MapId == mapId) + .Select(element => new ElementDto() + { + Id = element.Id, + MapId = element.MapId, + ModelId = element.ModelId, + Name = element.Name, + NodeId = element.NodeId, + OffsetX = element.OffsetX, + OffsetY = element.OffsetY, + IsOpen = element.IsOpen, + Content = element.Content + }); + return new(true, "") { Data = [..elements] }; + } + + public async Task> GetMapInfoByName(string name) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Name == name); + if (map == null) return new MessageResult(false, $"Không tìm thấy map {name}"); + + return new(true) + { + Data = new MapInfoDto() + { + Id = map.Id, + Name = map.Name, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + }, + }; + } + + public async Task> GetMapInfoById(Guid id) + { + var map = await MapDb.Maps.FirstOrDefaultAsync(m => m.Id == id); + if (map == null) return new MessageResult(false, $"Không tìm thấy map {id}"); + + return new(true) + { + Data = new MapInfoDto() + { + Id = map.Id, + Name = map.Name, + Active = map.Active, + OriginX = map.OriginX, + OriginY = map.OriginY, + Width = Math.Round(map.ImageWidth * map.Resolution, 2), + Height = Math.Round(map.ImageHeight * map.Resolution, 2), + Resolution = map.Resolution, + }, + }; + } +} diff --git a/RobotNet.MapManager/Program.cs b/RobotNet.MapManager/Program.cs new file mode 100644 index 0000000..a39855f --- /dev/null +++ b/RobotNet.MapManager/Program.cs @@ -0,0 +1,94 @@ +using Microsoft.EntityFrameworkCore; +using NLog.Web; +using OpenIddict.Validation.AspNetCore; +using RobotNet.MapManager.Data; +using RobotNet.MapManager.Hubs; +using RobotNet.MapManager.Services; +using RobotNet.OpenIddictClient; +using System.Globalization; + +CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseNLog(); + +// builder.AddServiceDefaults(); + +// Add services to the container. +var openIddictOption = builder.Configuration.GetSection(nameof(OpenIddictClientProviderOptions)).Get() + ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + +builder.Services.AddControllers(); +builder.Services.AddSignalR(); +builder.Services.AddOpenIddict() + .AddValidation(options => + { + // Note: the validation handler uses OpenID Connect discovery + // to retrieve the address of the introspection endpoint. + options.SetIssuer(openIddictOption.Issuer); + options.AddAudiences(openIddictOption.Audiences); + + // Configure the validation handler to use introspection and register the client + // credentials used when communicating with the remote introspection endpoint. + options.UseIntrospection() + .SetClientId(openIddictOption.ClientId) + .SetClientSecret(openIddictOption.ClientSecret); + + // Register the System.Net.Http integration. + if (builder.Environment.IsDevelopment()) + { + options.UseSystemNetHttp(httpOptions => + { + httpOptions.ConfigureHttpClientHandler(context => + { + context.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true; + }); + }); + } + else + { + options.UseSystemNetHttp(); + } + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }); + +builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); +builder.Services.AddAuthorization(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + + +// Add services to the container. + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +Action appDbOptions = options => options.UseSqlServer(connectionString, b => b.MigrationsAssembly("RobotNet.MapManager")); +builder.Services.AddDbContext(appDbOptions); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(typeof(LoggerController<>)); + +var app = builder.Build(); +await app.Services.SeedMapManagerDbAsync(); +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseCors(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.MapHub("/hubs/map/data"); + +app.Run(); \ No newline at end of file diff --git a/RobotNet.MapManager/Properties/launchSettings.json b/RobotNet.MapManager/Properties/launchSettings.json new file mode 100644 index 0000000..72ddec9 --- /dev/null +++ b/RobotNet.MapManager/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "workingDirectory": "$(TargetDir)", + "applicationUrl": "https://localhost:7177", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RobotNet.MapManager/RobotNet.MapManager.csproj b/RobotNet.MapManager/RobotNet.MapManager.csproj new file mode 100644 index 0000000..90a262c --- /dev/null +++ b/RobotNet.MapManager/RobotNet.MapManager.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/RobotNet.MapManager/RobotNet.MapManager.http b/RobotNet.MapManager/RobotNet.MapManager.http new file mode 100644 index 0000000..7bb9ea1 --- /dev/null +++ b/RobotNet.MapManager/RobotNet.MapManager.http @@ -0,0 +1,6 @@ +@RobotNet.MapManager_HostAddress = http://localhost:5082 + +GET {{RobotNet.MapManager_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/RobotNet.MapManager/Services/LoggerController.cs b/RobotNet.MapManager/Services/LoggerController.cs new file mode 100644 index 0000000..7a76914 --- /dev/null +++ b/RobotNet.MapManager/Services/LoggerController.cs @@ -0,0 +1,108 @@ +namespace RobotNet.MapManager.Services; + +public class LoggerController(ILogger Logger) +{ + public event Action? LoggerUpdate; + public void Write(string message, LogLevel level) + { + switch (level) + { + case LogLevel.Trace: + Logger.LogTrace("{mes}", message); + break; + case LogLevel.Debug: + Logger.LogDebug("{mes}", message); + break; + case LogLevel.Information: + Logger.LogInformation("{mes}", message); + break; + case LogLevel.Warning: + Logger.LogWarning("{mes}", message); + break; + case LogLevel.Error: + Logger.LogError("{mes}", message); + break; + case LogLevel.Critical: + Logger.LogCritical("{mes}", message); + break; + } + LoggerUpdate?.Invoke(); + } + + public void Write(string message) + { + Write(message, LogLevel.Information); + } + + public async Task WriteAsync(string message) + { + var write = Task.Run(() => Write(message)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task TraceAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Trace)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task DebugAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Debug)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task InfoAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Information)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task WarningAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Warning)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task ErrorAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Error)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task CriticalAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Critical)); + await write.WaitAsync(CancellationToken.None); + } + + public void Trace(string message) + { + Write(message, LogLevel.Trace); + } + + public void Debug(string message) + { + Write(message, LogLevel.Debug); + } + + public void Info(string message) + { + Write(message, LogLevel.Information); + } + + public void Warning(string message) + { + Write(message, LogLevel.Warning); + } + + public void Error(string message) + { + Write(message, LogLevel.Error); + } + + public void Critical(string message) + { + Write(message, LogLevel.Critical); + } +} diff --git a/RobotNet.MapManager/Services/MapEditorStorageRepository.cs b/RobotNet.MapManager/Services/MapEditorStorageRepository.cs new file mode 100644 index 0000000..56ac108 --- /dev/null +++ b/RobotNet.MapManager/Services/MapEditorStorageRepository.cs @@ -0,0 +1,140 @@ +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; + +namespace RobotNet.MapManager.Services; + +public class MapEditorStorageRepository +{ + private class MinioOption + { + public bool UsingLocal { get; set; } + public string? Endpoint { get; set; } + public string? User { get; set; } + public string? Password { get; set; } + public string? Bucket { get; set; } + } + + private readonly IMinioClient MinioClient; + private readonly MinioOption MinioOptions = new(); + public MapEditorStorageRepository(IConfiguration configuration) + { + configuration.Bind("MinIO", MinioOptions); + MinioClient = new MinioClient() + .WithEndpoint(MinioOptions.Endpoint) + .WithCredentials(MinioOptions.User, MinioOptions.Password) + .WithSSL(false) + .Build(); + } + + public async Task<(bool, string)> UploadAsync(string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken) + { + try + { + if (!MinioOptions.UsingLocal) + { + var beArgs = new BucketExistsArgs().WithBucket(MinioOptions.Bucket); + bool found = await MinioClient.BucketExistsAsync(beArgs, cancellationToken).ConfigureAwait(false); + if (!found) + { + var mbArgs = new MakeBucketArgs() + .WithBucket(MinioOptions.Bucket); + await MinioClient.MakeBucketAsync(mbArgs, cancellationToken).ConfigureAwait(false); + } + var getListBucketsTask = await MinioClient.ListBucketsAsync(cancellationToken); + var putObjectArgs = new PutObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}") + .WithObjectSize(size) + .WithStreamData(data) + .WithContentType(contentType); + await MinioClient.PutObjectAsync(putObjectArgs, cancellationToken).ConfigureAwait(false); + return (true, ""); + } + + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (!Directory.Exists(mapImageFolder)) Directory.CreateDirectory(mapImageFolder); + var folderPath = Path.Combine(mapImageFolder, path); + if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); + var pathLocal = Path.Combine(folderPath, $"{objectName}.png"); + if (File.Exists($"{pathLocal}.bk")) File.Delete($"{pathLocal}.bk"); + if (File.Exists(pathLocal)) File.Move(pathLocal, $"{pathLocal}.bk"); + using (Stream fileStream = new FileStream(pathLocal, FileMode.Create)) + { + await data.CopyToAsync(fileStream, cancellationToken); + } + return (true, ""); + } + catch (MinioException ex) + { + return (false, ex.Message); + } + } + + public (bool usingLocal, string url) GetUrl(string path, string objectName) + { + try + { + if (!MinioOptions.UsingLocal) + { + var presignedGetObjectArgs = new PresignedGetObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}") + .WithExpiry(60 * 60 * 24); + var url = MinioClient.PresignedGetObjectAsync(presignedGetObjectArgs); + url.Wait(); + return (false, url.Result); + } + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (Directory.Exists(mapImageFolder)) + { + var folderPath = Path.Combine(mapImageFolder, path); + if (Directory.Exists(folderPath)) + { + var pathLocal = Path.Combine(folderPath, $"{objectName}.png"); + if (File.Exists(pathLocal)) + { + return (true, pathLocal); + } + } + } + return (true, ""); + } + catch (MinioException ex) + { + return (false, ex.Message); + } + } + + public async Task<(bool, string)> DeleteAsync(string path, string objectName, CancellationToken cancellationToken) + { + try + { + if (!MinioOptions.UsingLocal) + { + var removeObjectArgs = new RemoveObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}"); + await MinioClient.RemoveObjectAsync(removeObjectArgs, cancellationToken).ConfigureAwait(false); + return (true, ""); + } + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (Directory.Exists(mapImageFolder)) + { + var folderPath = Path.Combine(mapImageFolder, path); + if (Directory.Exists(folderPath)) + { + var pathLocal = Path.Combine(folderPath, $"{objectName}.png"); + if (File.Exists(pathLocal)) File.Delete(pathLocal); + if (File.Exists($"{pathLocal}.bk")) File.Delete($"{pathLocal}.bk"); + } + } + return (true, ""); + } + catch (MinioException ex) + { + return (false, ex.Message); + } + } + +} \ No newline at end of file diff --git a/RobotNet.MapManager/Services/ServerHelper.cs b/RobotNet.MapManager/Services/ServerHelper.cs new file mode 100644 index 0000000..454948f --- /dev/null +++ b/RobotNet.MapManager/Services/ServerHelper.cs @@ -0,0 +1,161 @@ +using RobotNet.MapManager.Data; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapManager.Services; + +public class ServerHelper +{ + public static Edge CreateEdge(Map map, Guid startNodeId, Guid endNodeId, TrajectoryDegree trajectoryDegree, double cpX1 = 0, double cpY1 = 0, double cpX2 = 0, double cpY2 = 0) + { + return new Edge() + { + MapId = map.Id, + StartNodeId = startNodeId, + EndNodeId = endNodeId, + TrajectoryDegree = trajectoryDegree, + DirectionAllowed = map.EdgeDirectionAllowedDefault, + RotationAllowed = map.EdgeRotationAllowedDefault, + MaxSpeed = trajectoryDegree == TrajectoryDegree.One ? map.EdgeStraightMaxSpeedDefault : map.EdgeCurveMaxSpeedDefault, + MaxRotationSpeed = map.EdgeMaxRotationSpeedDefault, + MaxHeight = map.EdgeMaxHeightDefault, + MinHeight = map.EdgeMinHeightDefault, + Actions = "[]", + AllowedDeviationXy = map.EdgeAllowedDeviationXyDefault, + AllowedDeviationTheta = map.EdgeAllowedDeviationThetaDefault, + ControlPoint1X = cpX1, + ControlPoint1Y = cpY1, + ControlPoint2X = cpX2, + ControlPoint2Y = cpY2, + }; + } + + public static Node CreateNode(Map map, double x, double y) + { + return new Node() + { + MapId = map.Id, + Name = map.NodeNameAutoGenerate ? $"{map.NodeNameTemplateDefault}{++map.NodeCount}" : string.Empty, + X = x, + Y = y, + Theta = 0, + AllowedDeviationXy = map.NodeAllowedDeviationXyDefault, + AllowedDeviationTheta = map.NodeAllowedDeviationThetaDefault, + Actions = "[]", + }; + } + + public static EdgeDto CreateEdgeDto(Edge edge, Node startNode, Node endNode) + { + return new EdgeDto() + { + Id = edge.Id, + MapId = edge.MapId, + StartNodeId = edge.StartNodeId, + EndNodeId = edge.EndNodeId, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + DirectionAllowed = edge.DirectionAllowed, + RotationAllowed = edge.RotationAllowed, + MaxSpeed = edge.MaxSpeed, + MaxRotationSpeed = edge.MaxRotationSpeed, + MaxHeight = edge.MaxHeight, + MinHeight = edge.MinHeight, + Actions = edge.Actions, + AllowedDeviationXy = edge.AllowedDeviationXy, + AllowedDeviationTheta = edge.AllowedDeviationTheta, + StartNode = new NodeDto() + { + Id = startNode.Id, + Name = startNode.Name, + MapId = startNode.MapId, + Theta = startNode.Theta, + X = startNode.X, + Y = startNode.Y, + Actions = startNode.Actions, + AllowedDeviationXy = startNode.AllowedDeviationXy, + AllowedDeviationTheta = startNode.AllowedDeviationTheta, + }, + EndNode = new NodeDto() + { + Id = endNode.Id, + Name = endNode.Name, + MapId = endNode.MapId, + Theta = endNode.Theta, + X = endNode.X, + Y = endNode.Y, + AllowedDeviationXy = endNode.AllowedDeviationXy, + AllowedDeviationTheta = endNode.AllowedDeviationTheta, + Actions = endNode.Actions, + }, + }; + } + + public static NodeDto CreateNodeDto(Node node) + { + return new NodeDto() + { + Id = node.Id, + Name = node.Name, + MapId = node.MapId, + Theta = node.Theta, + X = node.X, + Y = node.Y, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions, + }; + } + + public static Edge? GetClosesEdge(double x, double y, IEnumerable nodes, IEnumerable edges, double allowDistance) + { + double minDistance = double.MaxValue; + Edge? edgeResult = null; + foreach (var edge in edges) + { + if (edge is not null) + { + var startNode = nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) continue; + var distance = DistanceToSegment(x, y, startNode.X, startNode.Y, endNode.X, endNode.Y); + if (distance < minDistance) + { + minDistance = distance; + edgeResult = edge; + } + } + } + if (minDistance > allowDistance) return null; + return edgeResult; + } + + private static double DistanceToSegment(double x, double y, double x1, double y1, double x2, double y2) + { + double dx = x2 - x1; + double dy = y2 - y1; + + double segmentLengthSquared = dx * dx + dy * dy; + + double t; + if (segmentLengthSquared == 0) + { + t = 0; + } + else + { + t = ((x - x1) * dx + (y - y1) * dy) / segmentLengthSquared; + t = Math.Max(0, Math.Min(1, t)); + } + + double nearestX = x1 + t * dx; + double nearestY = y1 + t * dy; + + double distanceSquared = (x - nearestX) * (x - nearestX) + (y - nearestY) * (y - nearestY); + + return Math.Sqrt(distanceSquared); + } +} diff --git a/RobotNet.MapManager/appsettings.json b/RobotNet.MapManager/appsettings.json new file mode 100644 index 0000000..512a552 --- /dev/null +++ b/RobotNet.MapManager/appsettings.json @@ -0,0 +1,31 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=172.20.235.170;Database=RobotNet.MapEditor;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "System.Net.Http.HttpClient": "Warning", + "OpenIddict.Validation.OpenIddictValidationDispatcher": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Warning" + } + }, + "AllowedHosts": "*", + "MinIO": { + "UsingLocal": false, + "Endpoint": "172.20.235.170:9000", + "Bucket": "mapeditor", + "User": "minio", + "Password": "robotics" + }, + "OpenIddictClientProviderOptions": { + "Issuer": "https://localhost:7061/", + "Audiences": [ + "robotnet-map-manager" + ], + "ClientId": "robotnet-map-manager", + "ClientSecret": "72B36E68-2F2B-455B-858A-77B1DCC79979", + "Scopes": [ ] + } +} diff --git a/RobotNet.MapManager/nlog.config b/RobotNet.MapManager/nlog.config new file mode 100644 index 0000000..e98f1ea --- /dev/null +++ b/RobotNet.MapManager/nlog.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.MapShares/Dtos/ActionDto.cs b/RobotNet.MapShares/Dtos/ActionDto.cs new file mode 100644 index 0000000..256607d --- /dev/null +++ b/RobotNet.MapShares/Dtos/ActionDto.cs @@ -0,0 +1,31 @@ +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class ActionDto +{ + public Guid Id { get; set; } + + public Guid MapId { get; set; } + + public string Name { get; set; } + + public string Content { get; set; } +} + +public class ActionCreateModel : ActionDto { } + +public class ActionModel +{ + public string ActionId { get; set; } = ""; + public string ActionType { get; set; } = ""; + public string ActionDescription { get; set; } = ""; + public string BlockingType { get; set; } = ""; + public ActionParameter[] ActionParameters { get; set; } = []; +} + +public class ActionParameter +{ + public string Key { get; set; } = ""; + public string Value { get; set; } = ""; +} diff --git a/RobotNet.MapShares/Dtos/EdgeDto.cs b/RobotNet.MapShares/Dtos/EdgeDto.cs new file mode 100644 index 0000000..045a68b --- /dev/null +++ b/RobotNet.MapShares/Dtos/EdgeDto.cs @@ -0,0 +1,86 @@ +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapShares.Dtos; + +public class EdgeDto +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public Guid StartNodeId { get; set; } + public Guid EndNodeId { get; set; } + + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } + public TrajectoryDegree TrajectoryDegree { get; set; } + + public double MaxHeight { get; set; } + public double MinHeight { get; set; } + public DirectionAllowed DirectionAllowed { get; set; } + public bool RotationAllowed { get; set; } + public double MaxRotationSpeed { get; set; } + public double MaxSpeed { get; set; } + public double AllowedDeviationXy { get; set; } + public double AllowedDeviationTheta { get; set; } + + public string Actions { get; set; } = ""; + + public NodeDto? StartNode { get; set; } + public NodeDto? EndNode { get; set; } +} + +public class EdgeCreateModel +{ + public Guid MapId { get; set; } + public double X1 { get; set; } + public double Y1 { get; set; } + public double X2 { get; set; } + public double Y2 { get; set; } + public TrajectoryDegree TrajectoryDegree { get; set; } + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } +} + +public class EdgeCreateDto +{ + public IEnumerable EdgesDto { get; set; } = []; + public IEnumerable RemoveEdge { get; set; } = []; +} + +public class EdgeUpdateModel +{ + public Guid Id { get; set; } + public double MaxSpeed { get; set; } + public double MaxHeight { get; set; } + public double MinHeight { get; set; } + public bool RotationAllowed { get; set; } + public double MaxRotationSpeed { get; set; } + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } + public DirectionAllowed DirectionAllowed { get; set; } + public Guid[] Actions { get; set; } = []; + public double AllowedDeviationXy { get; set; } + public double AllowedDeviationTheta { get; set; } +} + +public class EdgeMapCopyModel : EdgeCreateModel +{ + public Guid StartNodeId { get; set; } + public Guid EndNodeId { get; set; } + public double MaxSpeed { get; set; } + public double MaxHeight { get; set; } + public double MinHeight { get; set; } + public bool RotationAllowed { get; set; } + public double MaxRotationSpeed { get; set; } + public DirectionAllowed DirectionAllowed { get; set; } + public string Actions { get; set; } = ""; + public double AllowedDeviationXy { get; set; } + public double AllowedDeviationTheta { get; set; } +} + +public class EdgeCaculatorModel : EdgeCreateModel { } \ No newline at end of file diff --git a/RobotNet.MapShares/Dtos/ElementDto.cs b/RobotNet.MapShares/Dtos/ElementDto.cs new file mode 100644 index 0000000..56ac242 --- /dev/null +++ b/RobotNet.MapShares/Dtos/ElementDto.cs @@ -0,0 +1,49 @@ +using RobotNet.MapShares.Property; + +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class ElementDto +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public Guid ModelId { get; set; } + public string ModelName { get; set; } + public Guid NodeId { get; set; } + public string Name { get; set; } + public string NodeName { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public double OffsetX { get; set; } + public double OffsetY { get; set; } + public bool IsOpen { get; set; } + public string Content { get; set; } +} + +public class ElementCreateModel +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public Guid ModelId { get; set; } + public Guid NodeId { get; set; } + public string Name { get; set; } + public double OffsetX { get; set; } + public double OffsetY { get; set; } +} + +public class ElementUpdateModel +{ + public Guid Id { get; set; } + public string Name { get; set; } + public double OffsetX { get; set; } + public double OffsetY { get; set; } + public bool IsOpen { get; set; } + public string Content { get; set; } +} + +public class ElementPropertyUpdateModel +{ + public ElementProperty[] Properties { get; set; } = []; +} \ No newline at end of file diff --git a/RobotNet.MapShares/Dtos/ElementModelDto.cs b/RobotNet.MapShares/Dtos/ElementModelDto.cs new file mode 100644 index 0000000..0b5db8d --- /dev/null +++ b/RobotNet.MapShares/Dtos/ElementModelDto.cs @@ -0,0 +1,31 @@ +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class ElementModelDto +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public string Name { get; set; } + public double Width { get; set; } + public double Height { get; set; } + public int Image1Width { get; set; } + public int Image1Height { get; set; } + public int Image2Width { get; set; } + public int Image2Height { get; set; } + public string Content { get; set; } +} + +public class ElementModelCreateModel +{ + public string Name { get; set; } + public Guid MapId { get; set; } + public double Width { get; set; } + public double Height { get; set; } +} + +public class ElementModelUpdateModel : ElementModelCreateModel +{ + public Guid Id { get; set; } + public string Content { get; set; } +} diff --git a/RobotNet.MapShares/Dtos/MapDataExportDto.cs b/RobotNet.MapShares/Dtos/MapDataExportDto.cs new file mode 100644 index 0000000..c205597 --- /dev/null +++ b/RobotNet.MapShares/Dtos/MapDataExportDto.cs @@ -0,0 +1,66 @@ +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class MapInfoExportDto +{ + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Resolution { get; set; } + public double ViewX { get; set; } + public double ViewY { get; set; } + public double ViewWidth { get; set; } + public double ViewHeight { get; set; } + public string VDA5050 { get; set; } +} + +public class ElementModelExportDto : ElementModelDto +{ + public byte[] ImageOpenData { get; set; } = []; + public byte[] ImageCloseData { get; set; } = []; +} + +public class MapDataExportDto +{ + public long NodeCount { get; set; } + public NodeDto[] Nodes { get; set; } = []; + public EdgeDto[] Edges { get; set; } = []; + public ZoneDto[] Zones { get; set; } = []; + public ActionDto[] Actions { get; set; } = []; + public ElementModelExportDto[] ElementModels { get; set; } = []; + public ElementDto[] Elements { get; set; } = []; + public byte[] ImageData { get; set; } = []; +} + +public class MapSettingExportDto +{ + public bool NodeNameAutoGenerate { get; set; } + public string NodeNameTemplate { get; set; } + public double NodeAllowedDeviationXy { get; set; } + public double NodeAllowedDeviationTheta { get; set; } + + public double EdgeMinLength { get; set; } + public double EdgeStraightMaxSpeed { get; set; } + public double EdgeCurveMaxSpeed { get; set; } + public double EdgeMaxHeight { get; set; } + public double EdgeMinHeight { get; set; } + public double EdgeMaxRotationSpeed { get; set; } + public DirectionAllowed EdgeDirectionAllowed { get; set; } + public bool EdgeRotationAllowed { get; set; } + public double EdgeAllowedDeviationXy { get; set; } + public double EdgeAllowedDeviationTheta { get; set; } + + public double ZoneMinSquare { get; set; } +} + +public class MapExportDto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } = ""; + public MapInfoExportDto Info { get; set; } = new(); + public MapDataExportDto Data { get; set; } = new(); + public MapSettingExportDto Setting { get; set; } = new(); +} diff --git a/RobotNet.MapShares/Dtos/MapDto.cs b/RobotNet.MapShares/Dtos/MapDto.cs new file mode 100644 index 0000000..46cd671 --- /dev/null +++ b/RobotNet.MapShares/Dtos/MapDto.cs @@ -0,0 +1,97 @@ +using RobotNet.MapShares.Enums; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class MapInfoDto +{ + public Guid Id { get; set; } + public Guid VersionId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Resolution { get; set; } + public double ViewX { get; set; } + public double ViewY { get; set; } + public double ViewWidth { get; set; } + public double ViewHeight { get; set; } + public double Width { get; set; } + public double Height { get; set; } + public bool Active { get; set; } + public string VDA5050 { get; set; } +} + +public class MapCreateModel +{ + public string Name { get; set; } + + [Required] + public double OriginX { get; set; } + + [Required] + public double OriginY { get; set; } + + [Required] + public double Resolution { get; set; } +} + +public class MapUpdateModel +{ + public Guid Id { get; set; } + public string Name { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Resolution { get; set; } +} + +public class MapActiveModel +{ + public Guid Id { get; set; } + public bool Active { get; set; } +} +public class MapDataDto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Resolution { get; set; } + public double ImageWidth { get; set; } + public double ImageHeight { get; set; } + public bool Active { get; set; } + + public NodeDto[] Nodes { get; set; } = []; + public EdgeDto[] Edges { get; set; } = []; + public ZoneDto[] Zones { get; set; } = []; + public ActionDto[] Actions { get; set; } = []; + public ElementDto[] Elements { get; set; } = []; +} + +public class MapSettingDefaultDto +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } = ""; + public bool Active { get; set; } + + public bool NodeNameAutoGenerate { get; set; } + public string NodeNameTemplate { get; set; } + public double NodeAllowedDeviationXy { get; set; } + public double NodeAllowedDeviationTheta { get; set; } + + public double EdgeMinLength { get; set; } + public double EdgeStraightMaxSpeed { get; set; } + public double EdgeCurveMaxSpeed { get; set; } + public double EdgeMaxHeight { get; set; } + public double EdgeMinHeight { get; set; } + public double EdgeMaxRotationSpeed { get; set; } + public DirectionAllowed EdgeDirectionAllowed { get; set; } + public bool EdgeRotationAllowed { get; set; } + public double EdgeAllowedDeviationXy { get; set; } + public double EdgeAllowedDeviationTheta { get; set; } + + public double ZoneMinSquare { get; set; } +} \ No newline at end of file diff --git a/RobotNet.MapShares/Dtos/NodeDto.cs b/RobotNet.MapShares/Dtos/NodeDto.cs new file mode 100644 index 0000000..ce59204 --- /dev/null +++ b/RobotNet.MapShares/Dtos/NodeDto.cs @@ -0,0 +1,34 @@ +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapShares.Dtos; + +#nullable disable + +public class NodeDto +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public string Name { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public Direction Direction { get; set; } + public double AllowedDeviationXy { get; set; } + public double AllowedDeviationTheta { get; set; } + public string Actions { get; set; } + public override string ToString() => Name; +} + +public class NodeCreateModel : NodeDto { } + +public class NodeUpdateModel +{ + public Guid Id { get; set; } + public string Name { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public double AllowedDeviationXy { get; set; } + public double AllowedDeviationTheta { get; set; } + public Guid[] Actions { get; set; } = []; +} \ No newline at end of file diff --git a/RobotNet.MapShares/Dtos/ZoneDto.cs b/RobotNet.MapShares/Dtos/ZoneDto.cs new file mode 100644 index 0000000..418a594 --- /dev/null +++ b/RobotNet.MapShares/Dtos/ZoneDto.cs @@ -0,0 +1,22 @@ +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapShares.Dtos; + +public class ZoneDto +{ + public Guid Id { get; set; } + public Guid MapId { get; set; } + public ZoneType Type { get; set; } + public string Name { get; set; } = string.Empty; + public double X1 { get; set; } + public double Y1 { get; set; } + public double X2 { get; set; } + public double Y2 { get; set; } + public double X3 { get; set; } + public double Y3 { get; set; } + public double X4 { get; set; } + public double Y4 { get; set; } +} + +public class ZoneCreateModel : ZoneDto { } +public class ZoneUpdateModel : ZoneDto { } \ No newline at end of file diff --git a/RobotNet.MapShares/Enums/AlignState.cs b/RobotNet.MapShares/Enums/AlignState.cs new file mode 100644 index 0000000..255a5e5 --- /dev/null +++ b/RobotNet.MapShares/Enums/AlignState.cs @@ -0,0 +1,13 @@ +namespace RobotNet.MapShares.Enums; + +public enum AlignState +{ + HorizontalLeft, + HorizontalRight, + VerticalTop, + VerticalBottom, + HorizontalCenter, + VerticalCenter, + SplitNode, + MergeNode, +} diff --git a/RobotNet.MapShares/Enums/BlockingType.cs b/RobotNet.MapShares/Enums/BlockingType.cs new file mode 100644 index 0000000..8f2817c --- /dev/null +++ b/RobotNet.MapShares/Enums/BlockingType.cs @@ -0,0 +1,7 @@ +namespace RobotNet.MapShares.Enums; +public enum BlockingType +{ + NONE, + SOFT, + HARD +} \ No newline at end of file diff --git a/RobotNet.MapShares/Enums/ControlState.cs b/RobotNet.MapShares/Enums/ControlState.cs new file mode 100644 index 0000000..85b89ea --- /dev/null +++ b/RobotNet.MapShares/Enums/ControlState.cs @@ -0,0 +1,16 @@ +namespace RobotNet.MapShares.Enums; + +public enum ControlState +{ + Undo, + Save, + FitScreen, + ShowName, + ShowGrid, + ShowMapSlam, + ZoomIn, + ZoomOut, + Delete, + CheckMap +} + diff --git a/RobotNet.MapShares/Enums/Direction.cs b/RobotNet.MapShares/Enums/Direction.cs new file mode 100644 index 0000000..2d17d6e --- /dev/null +++ b/RobotNet.MapShares/Enums/Direction.cs @@ -0,0 +1,9 @@ +namespace RobotNet.MapShares.Enums; + +public enum Direction +{ + FORWARD, + BACKWARD, + NONE +} + diff --git a/RobotNet.MapShares/Enums/DirectionAllowed.cs b/RobotNet.MapShares/Enums/DirectionAllowed.cs new file mode 100644 index 0000000..ae3bf99 --- /dev/null +++ b/RobotNet.MapShares/Enums/DirectionAllowed.cs @@ -0,0 +1,10 @@ +namespace RobotNet.MapShares.Enums; + +public enum DirectionAllowed +{ + None, + Forward, + Backward, + Both, +} + diff --git a/RobotNet.MapShares/Enums/EditorState.cs b/RobotNet.MapShares/Enums/EditorState.cs new file mode 100644 index 0000000..fd01086 --- /dev/null +++ b/RobotNet.MapShares/Enums/EditorState.cs @@ -0,0 +1,16 @@ +namespace RobotNet.MapShares.Enums; + +public enum EditorState +{ + View, + Scaner, + NavigationEdit, + CreateStraighEdge, + CreateCurveEdge, + CreateDoubleCurveEdge, + CreateZone, + SettingZone, + CheckError, + Move, + Copy, +} diff --git a/RobotNet.MapShares/Enums/TrajectoryDegree.cs b/RobotNet.MapShares/Enums/TrajectoryDegree.cs new file mode 100644 index 0000000..f3217ae --- /dev/null +++ b/RobotNet.MapShares/Enums/TrajectoryDegree.cs @@ -0,0 +1,8 @@ +namespace RobotNet.MapShares.Enums; + +public enum TrajectoryDegree +{ + One, + Two, + Three, +} diff --git a/RobotNet.MapShares/Enums/ZoneType.cs b/RobotNet.MapShares/Enums/ZoneType.cs new file mode 100644 index 0000000..442f469 --- /dev/null +++ b/RobotNet.MapShares/Enums/ZoneType.cs @@ -0,0 +1,34 @@ +namespace RobotNet.MapShares.Enums; + +public enum ZoneType +{ + ///

+ /// Khu vực vận hành. Di chuyển theo tốc độ cài đặt + /// + Operating = 0, + + /// + /// Khu vực vận hành có rủi ro. Tốc độ tối đa (trung bình) cho phép là 0.6 m/s + /// + OperatingHazard = 1, + + /// + /// Khu vực nguy hiểm. Tốc độ tối đa cho phép là 0.3 m/s (Sử dụng tại vị trí vào sạc hoặc những vị trí có không gian hẹp) + /// + Restricted = 2, + + /// + /// Khu vực chuyền tải. Tốc độ tối đa cho phép la 0.3 m/s (Sử dụng tại những vị trí trao đổii tải hoặc có tương tác) + /// + LoadTransfer = 3, + + /// + /// Khu vực đặc biệt, cấm người không có quyền trong khu vực này + /// + Confined = 4, + + /// + /// Khu vực cấm. Không cho robot di chuyển vào khu vực này + /// + Forbidden = 5, +} \ No newline at end of file diff --git a/RobotNet.MapShares/JsonOptionExtends.cs b/RobotNet.MapShares/JsonOptionExtends.cs new file mode 100644 index 0000000..cc3bba3 --- /dev/null +++ b/RobotNet.MapShares/JsonOptionExtends.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace RobotNet.MapShares; + +public class JsonOptionExtends +{ + public static readonly JsonSerializerOptions Read = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + + public static readonly JsonSerializerOptions Write = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; +} diff --git a/RobotNet.MapShares/MapEditorHelper.cs b/RobotNet.MapShares/MapEditorHelper.cs new file mode 100644 index 0000000..390a584 --- /dev/null +++ b/RobotNet.MapShares/MapEditorHelper.cs @@ -0,0 +1,259 @@ +using RobotNet.MapShares.Dtos; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +namespace RobotNet.MapShares; + +public partial class MapEditorHelper +{ + [GeneratedRegex(@"^[a-zA-Z0-9\-_]*$")] + private static partial Regex MyRegex(); + private static readonly Regex NameRegex = MyRegex(); + + public static (bool IsSuccess, string returnStr) NameChecking(string name) + { + if (string.IsNullOrEmpty(name)) return (false, "Tên không được để trống."); + if (name.Length < 3 || name.Length > 127) return (false, "Tên gồm từ 3 đến 127 ký tự"); + if (!NameRegex.IsMatch(name)) return (false, "Tên chỉ chứa các ký tự thường (a - z), các ký tự hoa (A - Z), chữ số và ký tự '-'"); + return (true, ""); + } + + public static IEnumerable NameValidation(string name) + { + if (string.IsNullOrEmpty(name)) yield return "Tên không được để trống."; + if (name.Length < 3 || name.Length > 127) yield return "Tên gồm từ 3 đến 127 ký tự"; + if (!NameRegex.IsMatch(name)) yield return "Tên chỉ chứa các ký tự thường (a - z), các ký tự hoa (A - Z), chữ số và ký tự '-'"; + } + + public static IEnumerable RobotIdValidation(string id) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) yield return "Id không được để trống."; + if (id is not null && (id.Length < 1 || id.Length > 127)) yield return "Id gồm từ 1 đến 127 ký tự"; + if (id is not null && !NameRegex.IsMatch(id)) yield return "Id chỉ chứa các ký tự thường (a - z), kĩ tự in hoa (A - Z), chữ số và ký tự '-'"; + } + + public static (double X, double Y) CaculateControlPoint(double x1, double y1, double x2, double y2, double angle, double length) + { + double dx = x2 - x1; + double dy = y2 - y1; + double a = Math.Atan2(dy, dx) + angle * Math.PI / 180; + double d = length * Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)); + return (x1 + Math.Cos(a) * d, y1 + Math.Sin(a) * d); + } + + public static (double X, double Y)? CaculateIntersectionPoint(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { + if (x1 == x2 && x3 == x4) + { + if (x1 == x3) + { + return (x1, y1); + } + return null; + } + + if (y1 == y2) + { + return (x3, y1); + } + + if (x3 == x4) + { + return (x3, y1); + } + + double mAB = (y2 - y1) / (x2 - x1); + double mCD = (y4 - y3) / (x4 - x3); + + if (mAB == mCD) + { + return null; + } + + double x = (mAB * x1 - mCD * x3 + y3 - y1) / (mAB - mCD); + double y = mAB * (x - x1) + y1; + + return (x, y); + } + + public static (double PX1, double PY1, double PX2, double PY2) CaculateDoubleControlPoint(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { + var (X, Y) = CaculateIntersection(x1, y1, x2, y2, x3, y3); + var cp2 = CaculateIntersection(x4, y4, x3, y3, x2, y2); + + return (X, Y, cp2.X, cp2.Y); + } + + private static (double X, double Y) CaculateIntersection(double x1, double y1, double x2, double y2, double x3, double y3) + { + double length = Math.Sqrt(Math.Pow(x3 - x2, 2) + Math.Pow(y3 - y2, 2)); + var angle = Math.Atan2(y2 - y1, x2 - x1); + + var x = x2 + length * Math.Cos(angle) * Math.Sqrt(2) / 2; + var y = y2 + length * Math.Sin(angle) * Math.Sqrt(2) / 2; + return (x, y); + } + + public static NodeDto? GetClosesNode(double x, double y, List nodes) + { + NodeDto? finalNode = null; + double minDistance = 99; + foreach (var node in nodes) + { + var distance = Math.Sqrt(Math.Pow(node.X - x, 2) + Math.Pow(node.Y - y, 2)); + if (distance < minDistance) + { + minDistance = distance; + finalNode = node; + } + } + if (minDistance <= 0.35 && finalNode is not null) return finalNode; + return null; + } + + public static bool NodeInScanZone(double xRef, double yRef, double x1, double y1, double x2, double y2) + { + double Xmin = Math.Min(x1, x2); + double Xmax = Math.Max(x1, x2); + double Ymin = Math.Min(y1, y2); + double Ymax = Math.Max(y1, y2); + + return (Xmin <= xRef && xRef <= Xmax) && (Ymin <= yRef && yRef <= Ymax); + } + + public static bool IsPointInside(double x, double y, ZoneDto zone) + { + int crossings = 0; + + crossings += DoesRayCross(zone.X1, zone.Y1, zone.X2, zone.Y2, x, y) ? 1 : 0; + crossings += DoesRayCross(zone.X2, zone.Y2, zone.X3, zone.Y3, x, y) ? 1 : 0; + crossings += DoesRayCross(zone.X3, zone.Y3, zone.X4, zone.Y4, x, y) ? 1 : 0; + crossings += DoesRayCross(zone.X4, zone.Y4, zone.X1, zone.Y1, x, y) ? 1 : 0; + + return crossings % 2 == 1; + } + + public static bool DoesRayCross(double x1, double y1, double x2, double y2, double x, double y) + { + if (Math.Min(y1, y2) <= y && y < Math.Max(y1, y2)) + { + double xIntersect = x1 + (y - y1) * (x2 - x1) / (y2 - y1); + return x < xIntersect; + } + return false; + } + + public static double CalculateQuadrilateralArea(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { + return 0.5 * Math.Abs(x1 * y2 + x2 * y3 + x3 * y4 + x4 * y1 - (y1 * x2 + y2 * x3 + y3 * x4 + y4 * x1)); + } + + public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2) + { + var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2; + var y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * controlPointY + t * t * y2; + return (x, y); + } + + public static (double x, double y) CurveDegreeThree(double t, double x1, double y1, double controlPoint1X, double controlPoint1Y, double controlPoint2X, double controlPoint2Y, double x2, double y2) + { + var x = Math.Pow(1 - t, 3) * x1 + 3 * Math.Pow(1 - t, 2) * t * controlPoint1X + 3 * Math.Pow(t, 2) * (1 - t) * controlPoint2X + Math.Pow(t, 3) * x2; ; + var y = Math.Pow(1 - t, 3) * y1 + 3 * Math.Pow(1 - t, 2) * t * controlPoint1Y + 3 * Math.Pow(t, 2) * (1 - t) * controlPoint2Y + Math.Pow(t, 3) * y2; + return (x, y); + } + + public static (double x, double y) Curve(double t, EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree == Enums.TrajectoryDegree.One) + { + return (edge.X1 + t * (edge.X2 - edge.X1), edge.Y1 + t * (edge.Y2 - edge.Y1)); + } + else if (edge.TrajectoryDegree == Enums.TrajectoryDegree.Two) + { + return CurveDegreeTwo(t, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.X2, edge.Y2); + } + else + { + return CurveDegreeThree(t, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.ControlPoint2X, edge.ControlPoint2Y, edge.X2, edge.Y2); + } + } + + public static double GetEdgeLength(EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree == Enums.TrajectoryDegree.One) + { + return Math.Round(Math.Sqrt(Math.Pow(edge.X1 - edge.X2, 2) + Math.Pow(edge.Y1 - edge.Y2, 2)), 3); + } + else if (edge.TrajectoryDegree == Enums.TrajectoryDegree.Two) + { + var length = Math.Sqrt(Math.Pow(edge.X1 - edge.X2, 2) + Math.Pow(edge.Y1 - edge.Y2, 2)); + if (length == 0) return 0; + double step = 0.1 / length; + double distance = 0; + + for (double t = step; t <= 1.001; t += step) + { + (double x1, double y1) = CurveDegreeTwo(t - step, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.X2, edge.Y2); + (double x2, double y2) = CurveDegreeTwo(t, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.X2, edge.Y2); + distance += Math.Sqrt(Math.Pow(x1 - x2, 2) + Math.Pow(y1 - y2, 2)); + } + + return Math.Round(distance, 3); + } + else + { + var length = Math.Sqrt(Math.Pow(edge.X1 - edge.X2, 2) + Math.Pow(edge.Y1 - edge.Y2, 2)); + if (length == 0) return 0; + double step = 0.1 / length; + double distance = 0; + for (double t = step; t <= 1.001; t += step) + { + var sTime = t - step; + (var sx, var sy) = CurveDegreeThree(1 - sTime, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.ControlPoint2X, edge.ControlPoint2Y, edge.X2, edge.Y2); + sTime = t; + (var ex, var ey) = CurveDegreeThree(1 - sTime, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.ControlPoint2X, edge.ControlPoint2Y, edge.X2, edge.Y2); + + distance += Math.Sqrt(Math.Pow(sx - ex, 2) + Math.Pow(sy - ey, 2)); + } + return Math.Round(distance, 3); + } + } + + public static NodeDto GetNearByNode(NodeDto orginNode, NodeDto node2, EdgeDto edge, [Range(0, 1)] double ratio) + { + NodeDto Start = edge.StartNodeId == orginNode.Id ? orginNode : node2; + NodeDto End = edge.StartNodeId == orginNode.Id ? node2 : orginNode; + var localRatio = edge.StartNodeId == orginNode.Id ? ratio : 1 - ratio; + bool isReverse = edge.StartNodeId != orginNode.Id && edge.TrajectoryDegree == Enums.TrajectoryDegree.Three; + var (x, y) = Curve(localRatio, new() + { + X1 = Start.X, + Y1 = Start.Y, + X2 = End.X, + Y2 = End.Y, + ControlPoint1X = !isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint1Y = !isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y, + ControlPoint2X = !isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint2Y = !isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + TrajectoryDegree = edge.TrajectoryDegree, + }); + return new() { X = x, Y = y }; + } + + public static double GetAngle(NodeDto originNode, NodeDto Node1, NodeDto Node2) + { + double BA_x = Node1.X - originNode.X; + double BA_y = Node1.Y - originNode.Y; + double BC_x = Node2.X - originNode.X; + double BC_y = Node2.Y - originNode.Y; + // Tính độ dài của các vector AB và BC + double lengthAB = Math.Sqrt(BA_x * BA_x + BA_y * BA_y); + double lengthBC = Math.Sqrt(BC_x * BC_x + BC_y * BC_y); + // Tính tích vô hướng của AB và BC + double dotProduct = BA_x * BC_x + BA_y * BC_y; + if (lengthAB * lengthBC == 0) return 0; + if (dotProduct / (lengthAB * lengthBC) > 1) return 0; + if (dotProduct / (lengthAB * lengthBC) < -1) return 180; + return Math.Acos(dotProduct / (lengthAB * lengthBC)) * (180.0 / Math.PI); + } +} diff --git a/RobotNet.MapShares/MapManagerExtensions.cs b/RobotNet.MapShares/MapManagerExtensions.cs new file mode 100644 index 0000000..a6501dc --- /dev/null +++ b/RobotNet.MapShares/MapManagerExtensions.cs @@ -0,0 +1,43 @@ +using RobotNet.MapShares.Property; +using RobotNet.Script.Expressions; + +namespace RobotNet.MapShares; + +public static class MapManagerExtensions +{ + public static Script.Expressions.ElementProperties GetElementProperties(bool isOpen, string content) + { + var dbool = new Dictionary(); + var ddouble = new Dictionary(); + var dint = new Dictionary(); + var dstr = new Dictionary(); + + var elementProperties = System.Text.Json.JsonSerializer.Deserialize(content, JsonOptionExtends.Read) ?? []; + + foreach (var ep in elementProperties) + { + if(ep.Type == "System.Boolean" || ep.Type == "Boolean") + { + dbool.Add(ep.Name, bool.Parse(ep.DefaultValue)); + } + else if(ep.Type == "System.Double" || ep.Type == "Double") + { + ddouble.Add(ep.Name, double.Parse(ep.DefaultValue)); + } + else if(ep.Type == "System.Int" || ep.Type == "Int") + { + dint.Add(ep.Name, int.Parse(ep.DefaultValue)); + } + else if(ep.Type == "System.String" || ep.Type == "String") + { + dstr.Add(ep.Name, ep.DefaultValue); + } + else + { + throw new InvalidOperationException($"Unsupported property type: {ep.Type}"); + } + } + + return new ElementProperties(isOpen, dbool, ddouble, dint, dstr); + } +} diff --git a/RobotNet.MapShares/Models/ElementExpressionModel.cs b/RobotNet.MapShares/Models/ElementExpressionModel.cs new file mode 100644 index 0000000..e403250 --- /dev/null +++ b/RobotNet.MapShares/Models/ElementExpressionModel.cs @@ -0,0 +1,8 @@ +namespace RobotNet.MapShares.Models; + +public class ElementExpressionModel +{ + public string MapName { get; set; } = string.Empty; + public string ModelName { get; set; } = string.Empty; + public string Expression { get; set; } = string.Empty; +} diff --git a/RobotNet.MapShares/Models/LoggerModel.cs b/RobotNet.MapShares/Models/LoggerModel.cs new file mode 100644 index 0000000..8b8133b --- /dev/null +++ b/RobotNet.MapShares/Models/LoggerModel.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.MapShares.Models; + +public class LoggerModel +{ + [JsonPropertyName("time")] + public string? Time { get; set; } + + [JsonPropertyName("level")] + public string? Level { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("exception")] + public string? Exception { get; set; } + + public string ColorClass => Level switch + { + "WARN" => "text-warning", + "INFO" => "text-info", + "DEBUG" => "text-success", + "ERROR" => "text-danger", + "FATAL" => "text-secondary", + _ => "text-muted", + }; + + public string BackgroundClass => Level switch + { + "WARN" => "bg-warning text-dark", + "INFO" => "bg-info text-dark", + "DEBUG" => "bg-success text-white", + "ERROR" => "bg-danger text-white", + "FATAL" => "bg-secondary text-white", + _ => "bg-dark text-white", + }; + + public bool HasException => !string.IsNullOrEmpty(Exception); +} diff --git a/RobotNet.MapShares/Models/MapEditorBackupModel.cs b/RobotNet.MapShares/Models/MapEditorBackupModel.cs new file mode 100644 index 0000000..5930bc2 --- /dev/null +++ b/RobotNet.MapShares/Models/MapEditorBackupModel.cs @@ -0,0 +1,75 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.MapShares.Models; + +public enum MapEditorBackupType +{ + Node, + ControlPoint1Edge, + ControlPoint2Edge, + Zone, + MoveNode, + MoveEdge, + Copy, + MergeNode, + SplitNode +} + +public class MapEditorBackup +{ + public MapEditorBackupType Type { get; set; } + public Guid Id { get; set; } + public object Obj { get; set; } = null!; +} + +public class MapEditorBackupModel +{ + public MapEditorBackup[] Steps { get; set; } = []; +} + +public class PositionBackup +{ + public Guid Id { get; set; } + public double X { get; set; } + public double Y { get; set; } +} + +public class EdgeBackup +{ + public Guid Id { get; set; } + public TrajectoryDegree TrajectoryDegree { get; set; } + public double StartX { get; set; } + public double StartY { get; set; } + public double EndX { get; set; } + public double EndY { get; set; } + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } +} + +public class ZoneShapeBackup +{ + public int NodeChange { get; set; } + public double X1 { get; set; } + public double Y1 { get; set; } + public double X2 { get; set; } + public double Y2 { get; set; } + public double X3 { get; set; } + public double Y3 { get; set; } + public double X4 { get; set; } + public double Y4 { get; set; } +} + +public class SplitNodeBackup +{ + public Guid NodeId { get; set; } + public Dictionary EdgeSplit { get; set; } = []; +} + +public class MergeNodeUpdate +{ + public Guid NodeId { get; set; } + public Dictionary EdgesMerge { get; set; } = []; +} \ No newline at end of file diff --git a/RobotNet.MapShares/Property/ElementProperty.cs b/RobotNet.MapShares/Property/ElementProperty.cs new file mode 100644 index 0000000..d6b52e1 --- /dev/null +++ b/RobotNet.MapShares/Property/ElementProperty.cs @@ -0,0 +1,42 @@ +namespace RobotNet.MapShares.Property; + +#nullable disable + +public class ElementProperty +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string DefaultValue { get; set; } + public bool ReadOnly { get; set; } + + private static readonly Dictionary TypeMap = new() + { + { "Boolean", typeof(bool) }, + { "Double", typeof(double) }, + { "Int", typeof(int) }, + { "String", typeof(string) }, + { "System.Boolean", typeof(bool) }, + { "System.Double", typeof(double) }, + { "System.Int", typeof(int) }, + { "System.String", typeof(string) }, + }; + + public static string GetPropertyTypeName(Type type) + { + return TypeMap.FirstOrDefault(kv => kv.Value == type).Key ?? "Unknown"; + } + + public static readonly Type[] PropertyTypes = [ + typeof(bool), + typeof(double), + typeof(int), + typeof(string), + ]; + + public Type GetPropertyType() + { + return TypeMap.TryGetValue(Type ?? string.Empty, out var type) ? type : typeof(object); + } + +} diff --git a/RobotNet.MapShares/RobotNet.MapShares.csproj b/RobotNet.MapShares/RobotNet.MapShares.csproj new file mode 100644 index 0000000..4d83f09 --- /dev/null +++ b/RobotNet.MapShares/RobotNet.MapShares.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/RobotNet.OpenIddictClient/OpenIddictClientProviderOptions.cs b/RobotNet.OpenIddictClient/OpenIddictClientProviderOptions.cs new file mode 100644 index 0000000..8ed0a64 --- /dev/null +++ b/RobotNet.OpenIddictClient/OpenIddictClientProviderOptions.cs @@ -0,0 +1,12 @@ +namespace RobotNet.OpenIddictClient; + +#nullable disable + +public class OpenIddictClientProviderOptions +{ + public string Issuer { get; set; } + public string[] Audiences { get; set; } + public string[] Scopes { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } +} diff --git a/RobotNet.OpenIddictClient/OpenIddictResourceOptions.cs b/RobotNet.OpenIddictClient/OpenIddictResourceOptions.cs new file mode 100644 index 0000000..741b59f --- /dev/null +++ b/RobotNet.OpenIddictClient/OpenIddictResourceOptions.cs @@ -0,0 +1,7 @@ +namespace RobotNet.OpenIddictClient; + +public class OpenIddictResourceOptions +{ + public string Url { get; set; } = ""; + public string[] Scopes { get; set; } = []; +} diff --git a/RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj b/RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/RobotNet.RobotManager/Controllers/OpenACSSettingsController.cs b/RobotNet.RobotManager/Controllers/OpenACSSettingsController.cs new file mode 100644 index 0000000..7ec645a --- /dev/null +++ b/RobotNet.RobotManager/Controllers/OpenACSSettingsController.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.RobotManager.Services; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotShares.OpenACS; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class OpenACSSettingsController(OpenACSManager OpenACSManager, LoggerController Logger) : ControllerBase +{ + [HttpGet] + public Task> GetOpenACSSettings() + { + try + { + var settings = new OpenACSSettingsDto + { + PublishSetting = new(OpenACSManager.PublishEnable, OpenACSManager.PublishURL, OpenACSManager.PublishURLUsed, OpenACSManager.PublishInterval), + TrafficSetting = new(OpenACSManager.TrafficEnable, OpenACSManager.TrafficURL, OpenACSManager.TrafficURLUsed) + }; + return Task.FromResult>(new(true, "") { Data = settings }); + } + catch (Exception ex) + { + Logger.Warning($"Lấy cấu hình OpenACS xảy ra lỗi: {ex.Message}"); + return Task.FromResult>(new(false, "Hệ thống có lỗi xảy ra")); + } + } + + [HttpPost] + [Route("publish")] + public async Task> UpdatePublishSetting([FromBody] OpenACSPublishSettingModel settingModel) + { + try + { + await OpenACSManager.UpdatePublishURL(settingModel.Url); + await OpenACSManager.UpdatePublishInterval(settingModel.Interval); + return new(true, "") { Data = new(OpenACSManager.PublishEnable, OpenACSManager.PublishURL, OpenACSManager.PublishURLUsed, OpenACSManager.PublishInterval) }; + } + catch (Exception ex) + { + Logger.Warning($"Cập nhật cấu hình publish OpenACS xảy ra lỗi: {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpPost] + [Route("traffic")] + public async Task> UpdateTrafficSetting([FromBody] OpenACSTrafficSettingModel settingModel) + { + try + { + await OpenACSManager.UpdateTrafficURL(settingModel.Url); + return new(true, "") { Data = new(OpenACSManager.TrafficEnable, OpenACSManager.TrafficURL, OpenACSManager.TrafficURLUsed) }; + } + catch (Exception ex) + { + Logger.Warning($"Cập nhật cấu hình traffic OpenACS xảy ra lỗi: {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [Route("publish")] + public async Task UpdatePublishEnable([FromQuery] bool enable) + { + try + { + await OpenACSManager.UpdatePublishEnable(enable); + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"Cập nhật cấu hình publish OpenACS xảy ra lỗi: {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [Route("publish/count")] + [AllowAnonymous] + public int GetStatusPublishCount([FromServices] OpenACSPublisher publisher) => publisher.PublishCount; + + [HttpGet] + [Route("traffic")] + public async Task UpdateTrafficEnable([FromQuery] bool enable) + { + try + { + await OpenACSManager.UpdateTrafficEnable(enable); + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"Cập nhật cấu hình publish OpenACS xảy ra lỗi: {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Controllers/RobotManagerController.cs b/RobotNet.RobotManager/Controllers/RobotManagerController.cs new file mode 100644 index 0000000..3886b07 --- /dev/null +++ b/RobotNet.RobotManager/Controllers/RobotManagerController.cs @@ -0,0 +1,179 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.MapShares.Models; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services; +using RobotNet.RobotShares.Models; +using RobotNet.Script.Expressions; +using RobotNet.Shares; +using Serialize.Linq.Serializers; +using System.Linq.Expressions; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class RobotManagerController(RobotEditorDbContext RobotDb, MapManager MapManager, Services.RobotManager RobotManager, ILogger Logger) : ControllerBase +{ + private static readonly ExpressionSerializer expressionSerializer; + + static RobotManagerController() + { + var jss = new Serialize.Linq.Serializers.JsonSerializer(); + expressionSerializer = new ExpressionSerializer(jss); + + expressionSerializer.AddKnownType(typeof(RobotState)); + } + + [HttpPost] + [Route("MoveToNode")] + public async Task MoveToNode([FromBody] RobotMoveToNodeModel model) + { + try + { + if (string.IsNullOrEmpty(model.NodeName)) return new(false, "NodeName cannot be empty.."); + + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var map = await MapManager.GetMapData(robot.MapId); + if (!map.IsSuccess) return new(false, map.Message); + if (map is null || map.Data is null) return new(false, "The robot has not been assigned a map."); + + var node = map.Data.Nodes.FirstOrDefault(n => n.Name == model.NodeName && n.MapId == map.Data.Id); + if (node is null) return new(false, "This Node does not exist."); + + if (!robotController.StateMsg.NewBaseRequest) return new(false, "The robot is busy."); + + var move = await robotController.MoveToNode(node.Name, model.Actions, model.LastAngle); + if (move.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới node {model.NodeName} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới node {model.NodeName} không thành công thành công do {move.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi MoveToNode Robot {RobotId} di chuyển tới node {NodeName} xảy ra ngoại lệ: {Message}", model.RobotId, model.NodeName, ex.Message); + return new(false, "An error occurred."); + } + } + + [HttpDelete] + [Route("MoveToNode/{robotId}")] + public async Task Cancel(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var cancel = await robotController.CancelOrder(); + if (cancel.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} không thành công do {cancel.Message}"); + return new(false, "Request failed."); + + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi Cancel Robot {RobotId} xảy ra ngoại lệ: {Message}", robotId, ex.Message); + return new(false, "An error occurred."); + } + } + + [HttpPost] + [Route("InstantActions")] + public async Task InstantAction([FromBody] RobotInstantActionModel model) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var instantAction = await robotController.InstantAction(model.Action, false); + if (instantAction.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Gửi Action Robot {model.RobotId} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Gửi Action Robot {model.RobotId} không thành công do {instantAction.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi InstantAction Robot {RobotId}, Action type {action} xảy ra ngoại lệ: {Message}", model.RobotId, model.Action.ActionType, ex.Message); + return new(false, "An error occurred."); + } + } + + [HttpGet] + [Route("State/{robotId}")] + public async Task> GetState(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null) return new(false, "The robot has not logged into the system."); + + return new(true) + { + Data = new() + { + RobotId = robotId, + MapId = robot.MapId, + IsOnline = robotController.IsOnline, + State = robotController.State, + OrderState = robotController.OrderState, + NewBaseRequest = robotController.StateMsg.NewBaseRequest, + NodeStates = [.. robotController.StateMsg.NodeStates], + EdgeStates = [.. robotController.StateMsg.EdgeStates], + Loads = [.. robotController.StateMsg.Loads], + ActionStates = robotController.ActionStates, + BatteryState = robotController.StateMsg.BatteryState, + Errors = [.. robotController.StateMsg.Errors], + Information = [.. robotController.StateMsg.Information], + SafetyState = robotController.StateMsg.SafetyState, + AgvPosition = robotController.VisualizationMsg.AgvPosition, + Velocity = robotController.VisualizationMsg.Velocity, + } + }; + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi GetState Robot {RobotId} xảy ra ngoại lệ: {Message}", robotId, ex.Message); + return new(false, "An error occurred."); + } + } + + [HttpPost] + [Route("Search")] + public async Task> SearchRobot([FromBody] RobotSearchExpressionModel model) + { + var expr = expressionSerializer.DeserializeText(model.Expression); + var lambda = (Expression>)expr; + + // Compile và chạy: + var func = lambda.Compile(); + return await RobotManager.SearchRobot(model.ModelName, model.MapName, func); + } +} diff --git a/RobotNet.RobotManager/Controllers/RobotManagerLoggerController.cs b/RobotNet.RobotManager/Controllers/RobotManagerLoggerController.cs new file mode 100644 index 0000000..4b1258d --- /dev/null +++ b/RobotNet.RobotManager/Controllers/RobotManagerLoggerController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.RobotManager.Services; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class RobotManagerLoggerController(LoggerController Logger) : ControllerBase +{ + private readonly string LoggerDirectory = "robotManagerlogs"; + private readonly string OpenACSLoggerDirectory = "openACSlogs"; + + [HttpGet] + public async Task> GetLogs([FromQuery(Name = "date")] DateTime date) + { + string temp = ""; + try + { + string fileName = $"{date:yyyy-MM-dd}.log"; + string path = Path.Combine(LoggerDirectory, fileName); + if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory))) + { + Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ."); + return []; + } + + if (!System.IO.File.Exists(path)) + { + Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}."); + return []; + } + + temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log"); + System.IO.File.Copy(path, temp); + + return await System.IO.File.ReadAllLinesAsync(temp); + } + catch(Exception ex) + { + Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + finally + { + if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp); + } + } + + [HttpGet] + [Route("open-acs")] + public async Task> GetLogsACS([FromQuery(Name = "date")] DateTime date) + { + string temp = ""; + try + { + string fileName = $"{date:yyyy-MM-dd}.log"; + string path = Path.Combine(OpenACSLoggerDirectory, fileName); + if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(OpenACSLoggerDirectory))) + { + Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ."); + return []; + } + + if (!System.IO.File.Exists(path)) + { + Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}."); + return []; + } + + temp = Path.Combine(OpenACSLoggerDirectory, $"{Guid.NewGuid()}.log"); + System.IO.File.Copy(path, temp); + + return await System.IO.File.ReadAllLinesAsync(temp); + } + catch (Exception ex) + { + Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + finally + { + if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp); + } + } +} diff --git a/RobotNet.RobotManager/Controllers/RobotModelsController.cs b/RobotNet.RobotManager/Controllers/RobotModelsController.cs new file mode 100644 index 0000000..6c7bee1 --- /dev/null +++ b/RobotNet.RobotManager/Controllers/RobotModelsController.cs @@ -0,0 +1,258 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services; +using RobotNet.RobotShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class RobotModelsController(RobotEditorDbContext RobotDb, RobotEditorStorageRepository RobotStorage, IHttpClientFactory HttpClientFactory, LoggerController Logger) : ControllerBase +{ + [HttpGet] + public async Task> GetRobotModels([FromQuery(Name = "txtSearch")] string? txtSearch) + { + try + { + if (string.IsNullOrWhiteSpace(txtSearch)) + { + return await (from robotmodel in RobotDb.RobotModels + select new RobotModelDto() + { + Id = robotmodel.Id, + ModelName = robotmodel.ModelName, + OriginX = robotmodel.OriginX, + OriginY = robotmodel.OriginY, + ImageWidth = robotmodel.ImageWidth, + ImageHeight = robotmodel.ImageHeight, + Width = Math.Round(robotmodel.Width, 2), + Length = Math.Round(robotmodel.Length, 2), + NavigationType = robotmodel.NavigationType, + }).ToListAsync(); + } + else + { + return await (from robotmodel in RobotDb.RobotModels + where !string.IsNullOrEmpty(robotmodel.ModelName) && robotmodel.ModelName.Contains(txtSearch) + select new RobotModelDto() + { + Id = robotmodel.Id, + ModelName = robotmodel.ModelName, + OriginX = robotmodel.OriginX, + OriginY = robotmodel.OriginY, + ImageWidth = robotmodel.ImageWidth, + ImageHeight = robotmodel.ImageHeight, + Width = Math.Round(robotmodel.Width, 2), + Length = Math.Round(robotmodel.Length, 2), + NavigationType = robotmodel.NavigationType, + }).ToListAsync(); + } + } + catch (Exception ex) + { + Logger.Warning($"GetRobotModels: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + } + + [HttpGet] + [Route("{robotModelId}")] + public async Task> GetRobotModel(Guid robotModelId) + { + try + { + var robotmodel = await RobotDb.RobotModels.FirstOrDefaultAsync(model => model.Id == robotModelId); + var robotmodelDto = robotmodel is null ? null : new RobotModelDto() + { + Id = robotmodel.Id, + ModelName = robotmodel.ModelName, + OriginX = robotmodel.OriginX, + OriginY = robotmodel.OriginY, + ImageWidth = robotmodel.ImageWidth, + ImageHeight = robotmodel.ImageHeight, + Width = Math.Round(robotmodel.Width, 2), + Length = Math.Round(robotmodel.Length, 2), + NavigationType = robotmodel.NavigationType, + }; + return new(true) { Data = robotmodelDto }; + } + catch (Exception ex) + { + Logger.Warning($"GetRobotModel {robotModelId}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [AllowAnonymous] + [Route("image/{robotmodelId}")] + public async Task GetImage(Guid robotmodelId) + { + try + { + var (usingLocal, url) = RobotStorage.GetUrl("RobotModels", robotmodelId.ToString()); + if (!usingLocal) + { + var http = HttpClientFactory.CreateClient(); + var imageBytes = await http.GetByteArrayAsync(url); + if (imageBytes != null && imageBytes.Length > 0) return File(imageBytes, "image/png"); + else return NotFound("Không thể lấy được ảnh map."); + } + if (System.IO.File.Exists(url)) return PhysicalFile(url, "image/png"); + else return NotFound(); + } + catch (Exception ex) + { + Logger.Warning($"GetImage {robotmodelId}: Hệ thống có lỗi xảy ra - {ex.Message}"); + return NotFound(); + } + } + + [HttpPost] + public async Task> CreateRobotModel([FromForm] RobotModelCreateModel robotmodel, [FromForm(Name = "Image")] IFormFile imageUpload) + { + try + { + if (imageUpload.ContentType != "image/png") return new(false, "Robot image format chỉ hỗ trợ định dạng image/png"); + if (await RobotDb.RobotModels.AnyAsync(model => model.ModelName == robotmodel.ModelName)) return new(false, "Tên của robot model đã tồn tại, Hãy đặt tên khác!"); + + var image = SixLabors.ImageSharp.Image.Load(imageUpload.OpenReadStream()); + var entityRobotModel = await RobotDb.RobotModels.AddAsync(new RobotModel() + { + ModelName = robotmodel.ModelName, + OriginX = robotmodel.OriginX, + OriginY = robotmodel.OriginY, + ImageHeight = image.Height, + ImageWidth = image.Width, + Length = robotmodel.Length, + Width = robotmodel.Width, + NavigationType = robotmodel.NavigationType, + }); + + await RobotDb.SaveChangesAsync(); + + var (isSuccess, message) = await RobotStorage.UploadAsync("RobotModels", $"{entityRobotModel.Entity.Id}", imageUpload.OpenReadStream(), imageUpload.Length, imageUpload.ContentType, CancellationToken.None); + if (!isSuccess) + { + RobotDb.RobotModels.Remove(entityRobotModel.Entity); + await RobotDb.SaveChangesAsync(); + return new(false, message); + } + + return new(true) + { + Data = new() + { + Id = entityRobotModel.Entity.Id, + ModelName = entityRobotModel.Entity.ModelName, + OriginX = entityRobotModel.Entity.OriginX, + OriginY = entityRobotModel.Entity.OriginY, + ImageWidth = entityRobotModel.Entity.ImageWidth, + ImageHeight = entityRobotModel.Entity.ImageHeight, + Width = Math.Round(entityRobotModel.Entity.Width, 2), + Length = Math.Round(entityRobotModel.Entity.Length, 2), + NavigationType = entityRobotModel.Entity.NavigationType, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"CreateRobotModel : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + [HttpPut] + public async Task> UpdateRobotModel([FromBody] RobotModelUpdateModel robotmodel) + { + try + { + var robotModelDb = await RobotDb.RobotModels.FindAsync(robotmodel.Id); + if (robotModelDb == null) return new(false, $"Không tồn tại robot model id = {robotmodel.Id}"); + + if (robotmodel.ModelName != robotModelDb.ModelName && await RobotDb.RobotModels.AnyAsync(m => m.ModelName == robotmodel.ModelName)) + { + return new(false, "Tên của robot model đã tồn tại, Hãy đặt tên khác!"); + } + + robotModelDb.ModelName = robotmodel.ModelName; + robotModelDb.OriginX = robotmodel.OriginX; + robotModelDb.OriginY = robotmodel.OriginY; + robotModelDb.Length = robotmodel.Length; + robotModelDb.Width = robotmodel.Width; + + await RobotDb.SaveChangesAsync(); + return new(true) + { + Data = new() + { + ModelName = robotModelDb.ModelName, + OriginX = robotModelDb.OriginX, + OriginY = robotModelDb.OriginY, + NavigationType = robotModelDb.NavigationType, + ImageWidth = robotModelDb.ImageWidth, + ImageHeight = robotModelDb.ImageHeight, + Width = Math.Round(robotModelDb.Width, 2), + Length = Math.Round(robotModelDb.Length, 2), + } + }; + } + catch (Exception ex) + { + Logger.Warning($"UpdateRobotModel : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpDelete] + [Route("{robotModelId}")] + public async Task DeleteRobotModel(Guid robotModelId) + { + try + { + var robotModelDb = await RobotDb.RobotModels.FindAsync(robotModelId); + if (robotModelDb == null) return new(false, $"Không tồn tại robot model id = {robotModelId}"); + + if (RobotDb.Robots.Any(r => r.ModelId == robotModelId)) return new(false, "Tồn tại robot đang sử dụng model này"); + + await RobotStorage.DeleteAsync("RobotModels", robotModelId.ToString(), CancellationToken.None); + RobotDb.RobotModels.Remove(robotModelDb); + await RobotDb.SaveChangesAsync(); + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"DeleteRobotModel {robotModelId} : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpPut] + [Route("image/{robotModelId}")] + public async Task UpdateImage(Guid robotModelId, [FromForm(Name = "image")] IFormFile image) + { + try + { + var robotModel = await RobotDb.RobotModels.FindAsync(robotModelId); + if (robotModel == null) return new(false, $"Không tồn tại robot model id = {robotModelId}"); + + var imageStream = image.OpenReadStream(); + var imageUpdate = SixLabors.ImageSharp.Image.Load(imageStream); + robotModel.ImageWidth = imageUpdate.Width; + robotModel.ImageHeight = imageUpdate.Height; + await RobotDb.SaveChangesAsync(); + + var (isSuccess, message) = await RobotStorage.UploadAsync("RobotModels", robotModelId.ToString(), image.OpenReadStream(), image.Length, image.ContentType, CancellationToken.None); + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"UpdateImage: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Controllers/RobotsController.cs b/RobotNet.RobotManager/Controllers/RobotsController.cs new file mode 100644 index 0000000..7791b58 --- /dev/null +++ b/RobotNet.RobotManager/Controllers/RobotsController.cs @@ -0,0 +1,246 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class RobotsController(RobotEditorDbContext RobotDb, Services.RobotManager RobotManager, MapManager MapManager, LoggerController Logger) : ControllerBase +{ + [HttpGet] + public async Task>> GetRobots() + { + try + { + var robots = await (from robot in RobotDb.Robots + where !string.IsNullOrEmpty(robot.Name) && !string.IsNullOrEmpty(robot.RobotId) + join robotmodel in RobotDb.RobotModels on robot.ModelId equals robotmodel.Id + select new RobotDto() + { + Id = robot.Id, + RobotId = robot.RobotId, + Name = robot.Name, + ModelName = robotmodel.ModelName, + ModelId = robotmodel.Id, + MapId = robot.MapId, + NavigationType = robotmodel.NavigationType, + Online = false, + }).ToListAsync(); + foreach (var robot in robots) + { + var map = await MapManager.GetMapInfo(robot.MapId); + if (map is not null && map.Data is not null) robot.MapName = map.Data.Name; + else robot.MapName = string.Empty; + var robotController = RobotManager[robot.RobotId]; + robot.Online = robotController is not null && robotController.IsOnline; + robot.State = robotController is not null ? robotController.State : "OFFLINE"; + } + return new(true) { Data = robots }; + } + catch (Exception ex) + { + Logger.Warning($"GetRobots: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [Route("{robotId}")] + public async Task> GetRobot(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(model => model.RobotId == robotId); + if (robot is not null) + { + var map = await MapManager.GetMapInfo(robot.MapId); + var robotModel = await RobotDb.RobotModels.FirstOrDefaultAsync(m => m.Id == robot.ModelId); + return new(true) + { + Data = new RobotDto() + { + Id = robot.Id, + RobotId = robot.RobotId, + Name = robot.Name, + ModelName = robotModel is not null ? robotModel.ModelName : string.Empty, + ModelId = robot.ModelId, + MapId = robot.MapId, + MapName = map is not null && map.Data is not null ? map.Data.Name : string.Empty, + NavigationType = robotModel is not null ? robotModel.NavigationType : NavigationType.Differential, + Online = RobotManager[robot.RobotId] is not null, + } + }; + } + return new(false, "RobotId không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"GetRobot: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpPost] + public async Task> CreateRobot([FromBody] RobotCreateModel robot) + { + try + { + if (await RobotDb.Robots.AnyAsync(r => r.Name == robot.Name)) return new(false, "Tên của robot đã tồn tại, Hãy đặt tên khác!"); + if (await RobotDb.Robots.AnyAsync(r => r.RobotId == robot.RobotId)) return new(false, "Robot Id đã tồn tại, Hãy đặt tên khác!"); + + var robotModel = await RobotDb.RobotModels.FirstOrDefaultAsync(m => m.Id == robot.ModelId); + if (robotModel is null) return new(false, "Robot Model không tồn tại"); + + var entityRobotModel = await RobotDb.Robots.AddAsync(new Robot() + { + RobotId = robot.RobotId, + Name = robot.Name, + ModelId = robot.ModelId, + }); + + await RobotDb.SaveChangesAsync(); + + return new(true) + { + Data = new RobotDto() + { + Id = entityRobotModel.Entity.Id, + RobotId = entityRobotModel.Entity.RobotId, + Name = entityRobotModel.Entity.Name, + ModelId = entityRobotModel.Entity.ModelId, + ModelName = robotModel.ModelName, + NavigationType = robotModel.NavigationType, + } + }; + } + catch (Exception ex) + { + Logger.Warning($"CreateRobot : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpPut] + public async Task UpdateRobot([FromBody] RobotUpdateModel robot) + { + try + { + var robotDb = await RobotDb.Robots.FindAsync(robot.RobotId); + if (robotDb == null) return new(false, $"Không tồn tại robot model id = {robot.RobotId}"); + + if (robotDb.Name != robot.Name && await RobotDb.Robots.AnyAsync(r => r.Name == robot.Name)) + { + return new(false, "Tên của robot đã tồn tại, Hãy đặt tên khác!"); + } + + robotDb.Name = robot.Name; + robotDb.ModelId = robot.ModelId; + robotDb.MapId = robot.MapId; + + await RobotDb.SaveChangesAsync(); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateRobot : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpDelete] + [Route("{robotId}")] + public async Task DeleteRobot(string robotId) + { + try + { + var robotDb = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robotDb == null) return new(false, $"Không tồn tại robot id = {robotId}"); + + RobotManager.DeleteRobot(robotId); + RobotDb.Robots.Remove(robotDb); + await RobotDb.SaveChangesAsync(); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"DeleteRobot : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [Route("Model/{robotId}")] + public async Task> GetModel(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(model => model.RobotId == robotId); + if (robot is not null) + { + var robotModel = await RobotDb.RobotModels.FirstOrDefaultAsync(m => m.Id == robot.ModelId); + if (robotModel is null) return new(false, "Robot model không tồn tại"); + return new(true) + { + Data = new RobotModelDto() + { + Id = robotModel.Id, + ModelName = robotModel.ModelName, + OriginX = robotModel.OriginX, + OriginY = robotModel.OriginY, + NavigationType = robotModel.NavigationType, + ImageWidth = robotModel.ImageWidth, + ImageHeight = robotModel.ImageHeight, + Width = robotModel.Width, + Length = robotModel.Length + } + }; + } + return new(false, "RobotId không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"GetModel : Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpPost] + [Route("simulation")] + public MessageResult CreateRobotSimulation([FromBody] IEnumerable robotIds) + { + try + { + RobotManager.AddRobotSimulation(robotIds); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"CreateRobotSimulation: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpGet] + [Route("simulation")] + [AllowAnonymous] + public MessageResult DeleteRobotSimulation([FromQuery] string robotId) + { + try + { + RobotManager.DeleteRobot(robotId); + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"CreateRobotSimulation: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Controllers/TrafficACSRequestController.cs b/RobotNet.RobotManager/Controllers/TrafficACSRequestController.cs new file mode 100644 index 0000000..bf5ff7c --- /dev/null +++ b/RobotNet.RobotManager/Controllers/TrafficACSRequestController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotShares.OpenACS; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class TrafficACSRequestController(TrafficACS TrafficACS, Services.RobotManager RobotManager) : ControllerBase +{ + [HttpPost] + public async Task> UpdateTrafficSetting([FromBody] TrafficACSRequestModel model) + => model.Type switch + { + TrafficRequestType.IN => await TrafficACS.RequestIn(model.RobotId, model.ZoneId), + TrafficRequestType.OUT => await TrafficACS.RequestOut(model.RobotId, model.ZoneId), + _ => await TrafficACS.RequestOut(model.RobotId, model.ZoneId), + }; + + [HttpGet] + public RobotACSLockedDto[] GetRobotLocked() => RobotManager.GetRobotACSLocked(); +} diff --git a/RobotNet.RobotManager/Controllers/TrafficManagerController.cs b/RobotNet.RobotManager/Controllers/TrafficManagerController.cs new file mode 100644 index 0000000..d3f7b49 --- /dev/null +++ b/RobotNet.RobotManager/Controllers/TrafficManagerController.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.RobotManager.Services; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Models; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class TrafficManagerController(TrafficManager TrafficManager, LoggerController Logger) : ControllerBase +{ + [HttpPut] + public MessageResult UpdateLocker(UpdateAgentLockerModel model) + { + try + { + if (TrafficManager.TrafficMaps.TryGetValue(model.MapId, out TrafficMap? trafficmap) && trafficmap is not null) + { + trafficmap.UpdateLocker(model.AgentId, [.. model.LockedNodes]); + } + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"UpdateLocker: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } + + [HttpDelete] + [Route("{mapId}/{robotId}")] + public MessageResult DeleteAgent([FromRoute] Guid mapId, [FromRoute] string robotId) + { + try + { + if (TrafficManager.TrafficMaps.TryGetValue(mapId, out TrafficMap? trafficmap) && trafficmap is not null) + { + trafficmap.Locked.Remove(robotId); + } + return TrafficManager.DeleteAgent(robotId); + } + catch (Exception ex) + { + Logger.Warning($"DeleteAgent: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, $"Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.Designer.cs b/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.Designer.cs new file mode 100644 index 0000000..f1ee496 --- /dev/null +++ b/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.Designer.cs @@ -0,0 +1,131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.RobotManager.Data; + +#nullable disable + +namespace RobotNet.RobotManager.Data.Migrations +{ + [DbContext(typeof(RobotEditorDbContext))] + [Migration("20250509040621_AddRobotDb")] + partial class AddRobotDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ChargerNode") + .HasColumnType("varchar(127)") + .HasColumnName("ChargerNode"); + + b.Property("HomeNode") + .HasColumnType("varchar(127)") + .HasColumnName("HomeNode"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("Name"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("RobotId"); + + b.HasKey("Id"); + + b.HasIndex("MapId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ImageHeight") + .HasColumnType("int") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("int") + .HasColumnName("ImageWidth"); + + b.Property("Length") + .HasColumnType("float") + .HasColumnName("Length"); + + b.Property("ModelName") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("ModelName"); + + b.Property("NavigationType") + .HasColumnType("int") + .HasColumnName("NavigationType"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.HasOne("RobotNet.RobotManager.Data.RobotModel", "Model") + .WithMany("Robots") + .HasForeignKey("MapId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Navigation("Robots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.cs b/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.cs new file mode 100644 index 0000000..abd52d4 --- /dev/null +++ b/RobotNet.RobotManager/Data/Migrations/20250509040621_AddRobotDb.cs @@ -0,0 +1,72 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.RobotManager.Data.Migrations +{ + /// + public partial class AddRobotDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RobotModels", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ModelName = table.Column(type: "varchar(127)", nullable: false), + OriginX = table.Column(type: "float", nullable: false), + OriginY = table.Column(type: "float", nullable: false), + Length = table.Column(type: "float", nullable: false), + Width = table.Column(type: "float", nullable: false), + ImageWidth = table.Column(type: "int", nullable: false), + ImageHeight = table.Column(type: "int", nullable: false), + NavigationType = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RobotModels", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Robots", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + RobotId = table.Column(type: "varchar(127)", nullable: false), + Name = table.Column(type: "varchar(127)", nullable: false), + ModelId = table.Column(type: "uniqueidentifier", nullable: false), + MapId = table.Column(type: "uniqueidentifier", nullable: false), + HomeNode = table.Column(type: "varchar(127)", nullable: true), + ChargerNode = table.Column(type: "varchar(127)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Robots", x => x.Id); + table.ForeignKey( + name: "FK_Robots_RobotModels_MapId", + column: x => x.MapId, + principalTable: "RobotModels", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_Robots_MapId", + table: "Robots", + column: "MapId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Robots"); + + migrationBuilder.DropTable( + name: "RobotModels"); + } + } +} diff --git a/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.Designer.cs b/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.Designer.cs new file mode 100644 index 0000000..44bdf40 --- /dev/null +++ b/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.Designer.cs @@ -0,0 +1,131 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.RobotManager.Data; + +#nullable disable + +namespace RobotNet.RobotManager.Data.Migrations +{ + [DbContext(typeof(RobotEditorDbContext))] + [Migration("20250509071716_fixRobotDb")] + partial class fixRobotDb + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ChargerNode") + .HasColumnType("varchar(127)") + .HasColumnName("ChargerNode"); + + b.Property("HomeNode") + .HasColumnType("varchar(127)") + .HasColumnName("HomeNode"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("Name"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("RobotId"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ImageHeight") + .HasColumnType("int") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("int") + .HasColumnName("ImageWidth"); + + b.Property("Length") + .HasColumnType("float") + .HasColumnName("Length"); + + b.Property("ModelName") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("ModelName"); + + b.Property("NavigationType") + .HasColumnType("int") + .HasColumnName("NavigationType"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.HasOne("RobotNet.RobotManager.Data.RobotModel", "Model") + .WithMany("Robots") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Navigation("Robots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.cs b/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.cs new file mode 100644 index 0000000..59ea316 --- /dev/null +++ b/RobotNet.RobotManager/Data/Migrations/20250509071716_fixRobotDb.cs @@ -0,0 +1,60 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.RobotManager.Data.Migrations +{ + /// + public partial class fixRobotDb : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Robots_RobotModels_MapId", + table: "Robots"); + + migrationBuilder.DropIndex( + name: "IX_Robots_MapId", + table: "Robots"); + + migrationBuilder.CreateIndex( + name: "IX_Robots_ModelId", + table: "Robots", + column: "ModelId"); + + migrationBuilder.AddForeignKey( + name: "FK_Robots_RobotModels_ModelId", + table: "Robots", + column: "ModelId", + principalTable: "RobotModels", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Robots_RobotModels_ModelId", + table: "Robots"); + + migrationBuilder.DropIndex( + name: "IX_Robots_ModelId", + table: "Robots"); + + migrationBuilder.CreateIndex( + name: "IX_Robots_MapId", + table: "Robots", + column: "MapId"); + + migrationBuilder.AddForeignKey( + name: "FK_Robots_RobotModels_MapId", + table: "Robots", + column: "MapId", + principalTable: "RobotModels", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/RobotNet.RobotManager/Data/Migrations/RobotEditorDbContextModelSnapshot.cs b/RobotNet.RobotManager/Data/Migrations/RobotEditorDbContextModelSnapshot.cs new file mode 100644 index 0000000..c5099be --- /dev/null +++ b/RobotNet.RobotManager/Data/Migrations/RobotEditorDbContextModelSnapshot.cs @@ -0,0 +1,128 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.RobotManager.Data; + +#nullable disable + +namespace RobotNet.RobotManager.Data.Migrations +{ + [DbContext(typeof(RobotEditorDbContext))] + partial class RobotEditorDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ChargerNode") + .HasColumnType("varchar(127)") + .HasColumnName("ChargerNode"); + + b.Property("HomeNode") + .HasColumnType("varchar(127)") + .HasColumnName("HomeNode"); + + b.Property("MapId") + .HasColumnType("uniqueidentifier") + .HasColumnName("MapId"); + + b.Property("ModelId") + .HasColumnType("uniqueidentifier") + .HasColumnName("ModelId"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("Name"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("RobotId"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("ImageHeight") + .HasColumnType("int") + .HasColumnName("ImageHeight"); + + b.Property("ImageWidth") + .HasColumnType("int") + .HasColumnName("ImageWidth"); + + b.Property("Length") + .HasColumnType("float") + .HasColumnName("Length"); + + b.Property("ModelName") + .IsRequired() + .HasColumnType("varchar(127)") + .HasColumnName("ModelName"); + + b.Property("NavigationType") + .HasColumnType("int") + .HasColumnName("NavigationType"); + + b.Property("OriginX") + .HasColumnType("float") + .HasColumnName("OriginX"); + + b.Property("OriginY") + .HasColumnType("float") + .HasColumnName("OriginY"); + + b.Property("Width") + .HasColumnType("float") + .HasColumnName("Width"); + + b.HasKey("Id"); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.Robot", b => + { + b.HasOne("RobotNet.RobotManager.Data.RobotModel", "Model") + .WithMany("Robots") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("RobotNet.RobotManager.Data.RobotModel", b => + { + b.Navigation("Robots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.RobotManager/Data/Robot.cs b/RobotNet.RobotManager/Data/Robot.cs new file mode 100644 index 0000000..f0acd03 --- /dev/null +++ b/RobotNet.RobotManager/Data/Robot.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RobotNet.RobotManager.Data; + +#nullable disable + +[Table("Robots")] +public class Robot +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("RobotId", TypeName = "varchar(127)")] + [Required] + public string RobotId { get; set; } = string.Empty; + + [Column("Name", TypeName = "varchar(127)")] + [Required] + public string Name { get; set; } + + [Column("ModelId", TypeName = "uniqueidentifier")] + [Required] + public Guid ModelId { get; set; } + + [Column("MapId", TypeName = "uniqueidentifier")] + [Required] + public Guid MapId { get; set; } + + [Column("HomeNode", TypeName = "varchar(127)")] + public string HomeNode { get; set; } + + [Column("ChargerNode", TypeName = "varchar(127)")] + public string ChargerNode { get; set; } + + public RobotModel Model { get; set; } +} diff --git a/RobotNet.RobotManager/Data/RobotEditorDbContext.cs b/RobotNet.RobotManager/Data/RobotEditorDbContext.cs new file mode 100644 index 0000000..231e1cf --- /dev/null +++ b/RobotNet.RobotManager/Data/RobotEditorDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.RobotManager.Data; + +public class RobotEditorDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Robots { get; private set; } + public DbSet RobotModels { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(e => e.Robots) + .WithOne(e => e.Model) + .HasForeignKey(e => e.ModelId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/RobotNet.RobotManager/Data/RobotManagerDbExtensions.cs b/RobotNet.RobotManager/Data/RobotManagerDbExtensions.cs new file mode 100644 index 0000000..67d6599 --- /dev/null +++ b/RobotNet.RobotManager/Data/RobotManagerDbExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.RobotManager.Data; + +public static class RobotManagerDbExtensions +{ + public static async Task SeedRobotManagerDbAsync(this IServiceProvider serviceProvider) + { + using var scope = serviceProvider.GetRequiredService().CreateScope(); + + using var appDb = scope.ServiceProvider.GetRequiredService(); + + await appDb.Database.MigrateAsync(); + //await appDb.Database.EnsureCreatedAsync(); + await appDb.SaveChangesAsync(); + } +} diff --git a/RobotNet.RobotManager/Data/RobotModel.cs b/RobotNet.RobotManager/Data/RobotModel.cs new file mode 100644 index 0000000..0208e2e --- /dev/null +++ b/RobotNet.RobotManager/Data/RobotModel.cs @@ -0,0 +1,51 @@ +using RobotNet.RobotShares.Enums; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RobotNet.RobotManager.Data; + +#nullable disable + +[Table("RobotModels")] +public class RobotModel +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("ModelName", TypeName = "varchar(127)")] + [Required] + public string ModelName { get; set; } + + [Column("OriginX", TypeName = "float")] + [Required] + public double OriginX { get; set; } + + [Column("OriginY", TypeName = "float")] + [Required] + public double OriginY { get; set; } + + [Column("Length", TypeName = "float")] + [Required] + public double Length { get; set; } + + [Column("Width", TypeName = "float")] + [Required] + public double Width { get; set; } + + [Column("ImageWidth", TypeName = "int")] + [Required] + public int ImageWidth { get; set; } + + [Column("ImageHeight", TypeName = "int")] + [Required] + public int ImageHeight { get; set; } + + [Column("NavigationType", TypeName = "int")] + [Required] + public NavigationType NavigationType { get; set; } + + public virtual ICollection Robots { get; } = []; +} diff --git a/RobotNet.RobotManager/Dockerfile b/RobotNet.RobotManager/Dockerfile new file mode 100644 index 0000000..b60aa79 --- /dev/null +++ b/RobotNet.RobotManager/Dockerfile @@ -0,0 +1,73 @@ +FROM alpine:3.22 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["RobotNet.RobotManager/RobotNet.RobotManager.csproj", "RobotNet.RobotManager/"] +COPY ["RobotNet.RobotShares/RobotNet.RobotShares.csproj", "RobotNet.RobotShares/"] +COPY ["RobotNet.MapShares/RobotNet.MapShares.csproj", "RobotNet.MapShares/"] +COPY ["RobotNet.Script/RobotNet.Script.csproj", "RobotNet.Script/"] +COPY ["RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj", "RobotNet.Script.Expressions/"] +COPY ["RobotNet.Shares/RobotNet.Shares.csproj", "RobotNet.Shares/"] +COPY ["RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj", "RobotNet.OpenIddictClient/"] +COPY ["RobotNet.Clients/RobotNet.Clients.csproj", "RobotNet.Clients/"] + +# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.RobotManager/RobotNet.RobotManager.csproj" +RUN dotnet restore "RobotNet.RobotManager/RobotNet.RobotManager.csproj" + +COPY RobotNet.RobotManager/ RobotNet.RobotManager/ +COPY RobotNet.RobotShares/ RobotNet.RobotShares/ +COPY RobotNet.MapShares/ RobotNet.MapShares/ +COPY RobotNet.Script/ RobotNet.Script/ +COPY RobotNet.Script.Expressions/ RobotNet.Script.Expressions/ +COPY RobotNet.Shares/ RobotNet.Shares/ +COPY RobotNet.OpenIddictClient/ RobotNet.OpenIddictClient/ +COPY RobotNet.Clients/ RobotNet.Clients/ + +RUN rm -rf ./RobotNet.RobotManager/bin +RUN rm -rf ./RobotNet.RobotManager/obj +RUN rm -rf ./RobotNet.RobotShares/bin +RUN rm -rf ./RobotNet.RobotShares/obj +RUN rm -rf ./RobotNet.MapShares/bin +RUN rm -rf ./RobotNet.MapShares/obj +RUN rm -rf ./RobotNet.Script/bin +RUN rm -rf ./RobotNet.Script/obj +RUN rm -rf ./RobotNet.Script.Expressions/bin +RUN rm -rf ./RobotNet.Script.Expressions/obj +RUN rm -rf ./RobotNet.Shares/bin +RUN rm -rf ./RobotNet.Shares/obj +RUN rm -rf ./RobotNet.OpenIddictClient/bin +RUN rm -rf ./RobotNet.OpenIddictClient/obj +RUN rm -rf ./RobotNet.Clients/bin +RUN rm -rf ./RobotNet.Clients/obj + +WORKDIR "/src/RobotNet.RobotManager" +RUN dotnet build "RobotNet.RobotManager.csproj" -c Release -o /app/build + +FROM build AS publish +WORKDIR /src/RobotNet.RobotManager +RUN dotnet publish "RobotNet.RobotManager.csproj" \ + -c Release \ + -o /app/publish \ + --runtime linux-musl-x64 \ + --self-contained true \ + /p:PublishTrimmed=false \ + /p:PublishReadyToRun=true + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish ./ + +RUN apk add --no-cache icu-libs tzdata ca-certificates + +RUN echo '#!/bin/sh' >> ./start.sh +RUN echo 'update-ca-certificates' >> ./start.sh +RUN echo 'exec ./RobotNet.RobotManager' >> ./start.sh + +RUN chmod +x ./RobotNet.RobotManager +RUN chmod +x ./start.sh + +# Use the start script to ensure certificates are updated before starting the application +EXPOSE 443 +ENTRYPOINT ["./start.sh"] diff --git a/RobotNet.RobotManager/HubClients/MapHubClient.cs b/RobotNet.RobotManager/HubClients/MapHubClient.cs new file mode 100644 index 0000000..525ce49 --- /dev/null +++ b/RobotNet.RobotManager/HubClients/MapHubClient.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.SignalR.Client; +using OpenIddict.Client; +using RobotNet.MapShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.HubClients; + +public class MapHubClient(IHttpClientFactory HttpFactory, OpenIddictClientService openIddictClient) + : OpenIddictHubClient(new Uri($"{HttpFactory.CreateClient("MapManagerAPI").BaseAddress}hubs/map/data").ToString(), openIddictClient, ["robotnet-map-api"]) +{ + public event Action? MapUpdated; + private IDisposable? disMapUpdated; + + + new public async Task StartAsync() + { + disMapUpdated = Connection.On("MapUpdated", mapId => MapUpdated?.Invoke(mapId)); + + await base.StartAsync(); + } + + new public async Task StopAsync() + { + if (disMapUpdated != null) + { + disMapUpdated.Dispose(); + disMapUpdated = null; + } + await base.StopAsync(); + } + + public async Task> GetMapData(Guid mapId) + => IsConnected ? await Connection.InvokeAsync>(nameof(GetMapData), mapId) : new(false, "Kết nối thất bại"); + + public async Task> GetElementsState(Guid mapId) + => IsConnected ? await Connection.InvokeAsync>(nameof(GetElementsState), mapId) : new(false, "Kết nối thất bại"); + public async Task> GetMapInfoByName(string name) + => IsConnected ? await Connection.InvokeAsync>(nameof(GetMapInfoByName), name) : new(false, "Kết nối thất bại"); + public async Task> GetMapInfoById(Guid mapId) + => IsConnected ? await Connection.InvokeAsync>(nameof(GetMapInfoById), mapId) : new(false, "Kết nối thất bại"); +} diff --git a/RobotNet.RobotManager/HubClients/OpenIddictHubClient.cs b/RobotNet.RobotManager/HubClients/OpenIddictHubClient.cs new file mode 100644 index 0000000..cbe15d8 --- /dev/null +++ b/RobotNet.RobotManager/HubClients/OpenIddictHubClient.cs @@ -0,0 +1,23 @@ +using OpenIddict.Client; +using RobotNet.Clients; + +namespace RobotNet.RobotManager.HubClients; + +public class OpenIddictHubClient(string url, OpenIddictClientService openIddictClient, string[] scopes) : HubClient(new Uri(url), + async () => + { + var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new() + { + Scopes = [.. scopes], + }); + if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null) + { + return null; + } + else + { + return result.AccessToken; + } + }) +{ +} diff --git a/RobotNet.RobotManager/Hubs/RobotHub.cs b/RobotNet.RobotManager/Hubs/RobotHub.cs new file mode 100644 index 0000000..ba705bd --- /dev/null +++ b/RobotNet.RobotManager/Hubs/RobotHub.cs @@ -0,0 +1,178 @@ +using Azure.Core.Serialization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services; +using RobotNet.RobotShares.VDA5050.InstantAction; +using RobotNet.Shares; +using System.Text.Json; + +namespace RobotNet.RobotManager.Hubs; + +[Authorize] +public class RobotHub(RobotPublisher RobotPublisher, Services.RobotManager RobotManager, RobotEditorDbContext RobotDb, LoggerController Logger) : Hub +{ + public async Task MapActive(Guid mapId) + { + var keysToRemove = RobotPublisher.MapActive.Where(kvp => kvp.Value == Context.ConnectionId) + .Select(kvp => kvp.Key) + .ToList(); + foreach (var key in keysToRemove) + { + RobotPublisher.MapActive.Remove(key); + } + + RobotPublisher.MapActive[mapId] = Context.ConnectionId; + await Clients.AllExcept([.. RobotPublisher.MapActive.Values]).SendAsync("MapDeactive"); + } + + public async Task RobotDetailActive(string robotId) + { + var keysToRemove = RobotPublisher.RobotDetailActive.Where(kvp => kvp.Value == Context.ConnectionId) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in keysToRemove) + { + RobotPublisher.RobotDetailActive.Remove(key); + } + + RobotPublisher.RobotDetailActive[robotId] = Context.ConnectionId; + await Clients.AllExcept([.. RobotPublisher.RobotDetailActive.Values]).SendAsync("RobotDetailDeactive"); + } + + public MessageResult SetInitialPose(string robotId, double x, double y, double yaw) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) + { + robot.Initialize(x, y, yaw * 180 / Math.PI); + return new(true); + } + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"SetInitialPose: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + public MessageResult MoveStraight(string robotId, double x, double y, double theta) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) return robot.MoveStraight(x, y); + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"MoveStraight: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task MoveToNode(string robotId, string nodename) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) return await robot.MoveToNode(nodename); + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"MoveToNode: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task MoveRandom(string robotId, List nodes) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) return await robot.MoveRandom(nodes); + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"MoveToNode: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task CancelNavigation(string robotId) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) return await robot.CancelOrder(); + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"CancelNavigation: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task SetMap(string robotId, Guid mapId) + { + try + { + var robot = RobotDb.Robots.FirstOrDefault(x => x.RobotId == robotId); + if (robot is not null) + { + robot.MapId = mapId; + await RobotDb.SaveChangesAsync(); + return new(true); + } + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"SetMap: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task SendAction(string robotId, ActionDto action) + { + try + { + var robot = RobotManager[robotId]; + var actionVDA = JsonSerializer.Deserialize(action.Content, JsonOptionExtends.Read); + if (robot is not null) + { + if (actionVDA is null) return new(false, "Action không hợp lệ"); + var send = await robot.InstantAction(actionVDA, false); + return new(send.IsSuccess, send.Message); + } + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"MoveToNode: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task CancelAction(string robotId) + { + try + { + var robot = RobotManager[robotId]; + if (robot is not null) return await robot.CancelAction(); + return new(false, "Robot không tồn tại"); + } + catch (Exception ex) + { + Logger.Warning($"CancelAction: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Hubs/RobotManagerHub.cs b/RobotNet.RobotManager/Hubs/RobotManagerHub.cs new file mode 100644 index 0000000..789dcaa --- /dev/null +++ b/RobotNet.RobotManager/Hubs/RobotManagerHub.cs @@ -0,0 +1,244 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; +using RobotNet.RobotManager.Controllers; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotShares.Models; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Hubs; + +[Authorize] +public class RobotManagerHub(RobotEditorDbContext RobotDb, MapManager MapManager, Services.RobotManager RobotManager, TrafficACS TrafficACS, ILogger Logger) : Hub +{ + public async Task MoveToNode(RobotMoveToNodeModel model) + { + try + { + if (string.IsNullOrEmpty(model.NodeName)) return new(false, "NodeName cannot be empty.."); + + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var map = await MapManager.GetMapData(robot.MapId); + if (!map.IsSuccess) return new(false, map.Message); + if (map is null || map.Data is null) return new(false, "The robot has not been assigned a map."); + + var node = map.Data.Nodes.FirstOrDefault(n => n.Name == model.NodeName && n.MapId == map.Data.Id); + if (node is null) return new(false, "This Node does not exist."); + + if (robotController.IsWorking) return new(false, "The robot is busy."); + + var move = await robotController.MoveToNode(node.Name, model.Actions, model.OverrideLastAngle ? model.LastAngle : null); + if (move.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới node {model.NodeName} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới node {model.NodeName} không thành công thành công do {move.Message}"); + return new(false, move.Message); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi MoveToNode Robot {RobotId} di chuyển tới node {NodeName} xảy ra lỗi: {Message}", model.RobotId, model.NodeName, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task MoveStraight(RobotMoveStraightModel model) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + if (robotController.IsWorking) return new(false, "The robot is busy."); + + var move = robotController.MoveStraight(model.X, model.Y); + if (move.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới tọa độ [{model.X} - {model.Y}] thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} di chuyển tới tọa độ [{model.X} - {model.Y}] không thành công thành công do {move.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi Rotate Robot {RobotId} di chuyển tới tọa độ [{model.X} - {model.Y}] xảy ra lỗi: {Message}", model.RobotId, model.X, model.Y, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task Rotate(RobotRotateModel model) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + if (robotController.IsWorking) return new(false, "The robot is busy."); + + var move = robotController.Rotate(model.Angle); + if (move.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} quay tới góc {model.Angle} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Goi Robot {model.RobotId} quay tới góc {model.Angle} không thành công thành công do {move.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi Rotate Robot {RobotId} quay tới góc {model.Angle} xảy ra lỗi: {Message}", model.RobotId, model.Angle, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task CancelOrder(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var cancel = await robotController.CancelOrder(); + if (cancel.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} không thành công do {cancel.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi Cancel Order Robot {RobotId} xảy ra lỗi: {Message}", robotId, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task CancelAction(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var cancel = await robotController.CancelAction(); + if (cancel.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} thành công "); + return new(true); + } + robotController.Log($"RobotManager API Controller Log: Hủy bỏ nhiệm vụ của Robot {robotId} không thành công do {cancel.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi Cancel Order Robot {RobotId} xảy ra lỗi: {Message}", robotId, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task> InstantAction(RobotInstantActionModel model) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == model.RobotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null || !robotController.IsOnline) return new(false, "The robot is not online."); + + var instantAction = await robotController.InstantAction(model.Action, false); + if (instantAction.IsSuccess) + { + robotController.Log($"RobotManager API Controller Log: Gửi Action Robot {model.RobotId} thành công "); + return instantAction; + } + robotController.Log($"RobotManager API Controller Log: Gửi Action Robot {model.RobotId} không thành công do {instantAction.Message}"); + return new(false, "Request failed."); + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi InstantAction Robot {RobotId}, Action type {action} xảy ra lỗi: {Message}", model.RobotId, model.Action.ActionType, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task> GetState(string robotId) + { + try + { + var robot = await RobotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == robotId); + if (robot is null) return new(false, "RobotId does not exist."); + + var robotController = RobotManager[robot.RobotId]; + if (robotController is null) return new(true, "") + { + Data = new() + { + RobotId = robotId, + IsOnline = false, + MapId = robot.MapId, + } + }; + + return new(true) + { + Data = new() + { + RobotId = robotId, + MapId = robot.MapId, + IsOnline = robotController.IsOnline, + State = robotController.State, + OrderState = robotController.OrderState, + NewBaseRequest = robotController.StateMsg.NewBaseRequest, + NodeStates = [.. robotController.StateMsg.NodeStates], + EdgeStates = [.. robotController.StateMsg.EdgeStates], + Loads = [.. robotController.StateMsg.Loads], + ActionStates = robotController.ActionStates, + BatteryState = robotController.StateMsg.BatteryState, + Errors = [.. robotController.StateMsg.Errors], + Information = [.. robotController.StateMsg.Information], + SafetyState = robotController.StateMsg.SafetyState, + AgvPosition = robotController.VisualizationMsg.AgvPosition, + Velocity = robotController.VisualizationMsg.Velocity, + } + }; + } + catch (Exception ex) + { + Logger.LogError("RobotManager API Controller Log: Goi GetState Robot {RobotId} xảy ra lỗi: {Message}", robotId, ex.Message); + return new(false, "An error occurred."); + } + } + + public async Task RequestACSIn(string robotId, string id) + { + var result = await TrafficACS.RequestIn(robotId, id); + return result.Data; + } + + public async Task RequestACSOut(string robotId, string id) + { + var result = await TrafficACS.RequestOut(robotId, id); + return result.Data; + } +} diff --git a/RobotNet.RobotManager/Hubs/TrafficHub.cs b/RobotNet.RobotManager/Hubs/TrafficHub.cs new file mode 100644 index 0000000..9e5340c --- /dev/null +++ b/RobotNet.RobotManager/Hubs/TrafficHub.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.RobotManager.Services; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Hubs; + +[Authorize] +public class TrafficHub(TrafficPublisher TrafficPublisher, TrafficManager TrafficManager, MapManager MapManager, LoggerController Logger) : Hub +{ + public async Task TrafficActive(Guid mapId) + { + var keysToRemove = TrafficPublisher.TrafficMapActive.Where(kvp => kvp.Value == Context.ConnectionId) + .Select(kvp => kvp.Key) + .ToList(); + foreach (var key in keysToRemove) + { + TrafficPublisher.TrafficMapActive.Remove(key); + } + + TrafficPublisher.TrafficMapActive[mapId] = Context.ConnectionId; + await Clients.AllExcept([.. TrafficPublisher.TrafficMapActive.Values]).SendAsync("TrafficManagerDeactive"); + } + + public async Task>> LoadTrafficMaps() + { + try + { + List trafficMaps = []; + foreach (var trafficMap in TrafficManager.TrafficMaps) + { + var map = await MapManager.GetMapData(trafficMap.Key); + trafficMaps.Add(new() + { + MapId = trafficMap.Key, + Agents = TrafficPublisher.GetAgents(trafficMap.Key), + MapName = map.Data is null ? "" : map.Data.Name, + }); + } + return new(true) { Data = trafficMaps }; + } + catch (Exception ex) + { + Logger.Warning($"LoadTrafficMaps: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public async Task> LoadTrafficMap(Guid mapId) + { + try + { + var map = await MapManager.GetMapData(mapId); + var trafficMap = new TrafficMapDto() + { + MapId = mapId, + Agents = TrafficPublisher.GetAgents(mapId), + MapName = map.Data is null ? "" : map.Data.Name, + }; + return new(true) { Data = trafficMap }; + } + catch (Exception ex) + { + Logger.Warning($"LoadTrafficMaps: Hệ thống có lỗi xảy ra - {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.RobotManager/Program.cs b/RobotNet.RobotManager/Program.cs new file mode 100644 index 0000000..68ec47e --- /dev/null +++ b/RobotNet.RobotManager/Program.cs @@ -0,0 +1,159 @@ +using Microsoft.EntityFrameworkCore; +using NLog.Web; +using OpenIddict.Client; +using OpenIddict.Validation.AspNetCore; +using RobotNet.OpenIddictClient; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.HubClients; +using RobotNet.RobotManager.Hubs; +using RobotNet.RobotManager.Services; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotManager.Services.Traffic; +using System.Net.Http.Headers; +using static OpenIddict.Abstractions.OpenIddictConstants; + +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseNLog(); + +//builder.AddServiceDefaults(); + +// Add services to the container. + +builder.Services.AddControllers(); +builder.Services.AddSignalR(); + +var openIddictOption = builder.Configuration.GetSection(nameof(OpenIddictClientProviderOptions)).Get() + ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + +builder.Services.AddOpenIddict() + .AddValidation(options => + { + // Note: the validation handler uses OpenID Connect discovery + // to retrieve the address of the introspection endpoint. + options.SetIssuer(openIddictOption.Issuer); + options.AddAudiences(openIddictOption.Audiences); + + // Configure the validation handler to use introspection and register the client + // credentials used when communicating with the remote introspection endpoint. + options.UseIntrospection() + .SetClientId(openIddictOption.ClientId) + .SetClientSecret(openIddictOption.ClientSecret); + + // Register the System.Net.Http integration. + if (builder.Environment.IsDevelopment()) + { + options.UseSystemNetHttp(httpOptions => + { + httpOptions.ConfigureHttpClientHandler(context => + { + context.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true; + }); + }); + } + else + { + options.UseSystemNetHttp(); + } + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }) + .AddClient(options => + { + // Allow grant_type=client_credentials to be negotiated. + options.AllowClientCredentialsFlow(); + + // Disable token storage, which is not necessary for non-interactive flows like + // grant_type=password, grant_type=client_credentials or grant_type=refresh_token. + options.DisableTokenStorage(); + + // Register the System.Net.Http integration and use the identity of the current + // assembly as a more specific user agent, which can be useful when dealing with + // providers that use the user agent as a way to throttle requests (e.g Reddit). + options.UseSystemNetHttp() + .SetProductInformation(typeof(Program).Assembly); + + var registration = new OpenIddictClientRegistration + { + Issuer = new Uri(openIddictOption.Issuer, UriKind.Absolute), + GrantTypes = { GrantTypes.ClientCredentials }, + ClientId = openIddictOption.ClientId, + ClientSecret = openIddictOption.ClientSecret, + }; + + foreach (var scope in openIddictOption.Scopes) + { + registration.Scopes.Add(scope); + } + + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(registration); + }); + +builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); +builder.Services.AddAuthorization(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var mapManagerOptions = builder.Configuration.GetSection("MapManager").Get() + ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + +//builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); +builder.Services.AddHttpClient("MapManagerAPI", client => +{ + client.BaseAddress = new Uri(mapManagerOptions.Url); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); +}).AddHttpMessageHandler(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); +Action appDbOptions = options => options.UseSqlServer(connectionString, b => b.MigrationsAssembly("RobotNet.RobotManager")); +builder.Services.AddDbContext(appDbOptions); + +builder.Services.AddTransient(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(typeof(LoggerController<>)); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); + +var app = builder.Build(); +await app.Services.SeedRobotManagerDbAsync(); +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseCors(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.MapHub("/hubs/robot/online"); +app.MapHub("/hubs/traffic"); +app.MapHub("/hubs/robot-manager"); + +app.Run(); diff --git a/RobotNet.RobotManager/Properties/launchSettings.json b/RobotNet.RobotManager/Properties/launchSettings.json new file mode 100644 index 0000000..cee494b --- /dev/null +++ b/RobotNet.RobotManager/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "workingDirectory": "$(TargetDir)", + "applicationUrl": "https://localhost:7179", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RobotNet.RobotManager/RobotNet.RobotManager.csproj b/RobotNet.RobotManager/RobotNet.RobotManager.csproj new file mode 100644 index 0000000..536680f --- /dev/null +++ b/RobotNet.RobotManager/RobotNet.RobotManager.csproj @@ -0,0 +1,35 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/RobotNet.RobotManager/RobotNet.RobotManager.http b/RobotNet.RobotManager/RobotNet.RobotManager.http new file mode 100644 index 0000000..b402a2a --- /dev/null +++ b/RobotNet.RobotManager/RobotNet.RobotManager.http @@ -0,0 +1,6 @@ +@RobotNet.RobotManager_HostAddress = http://localhost:5167 + +GET {{RobotNet.RobotManager_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/RobotNet.RobotManager/Services/IRobotController.cs b/RobotNet.RobotManager/Services/IRobotController.cs new file mode 100644 index 0000000..b289725 --- /dev/null +++ b/RobotNet.RobotManager/Services/IRobotController.cs @@ -0,0 +1,36 @@ +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.VDA5050.Factsheet; +using RobotNet.RobotShares.VDA5050.FactsheetExtend; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services; + +public interface IRobotController +{ + string SerialNumber { get; } + bool IsOnline { get; set; } + bool IsWorking { get; } + string State { get; } + RobotOrderDto OrderState { get; } + RobotActionDto[] ActionStates { get; } + AutoResetEvent RobotUpdated { get; set; } + StateMsg StateMsg { get; set; } + VisualizationMsg VisualizationMsg { get; set; } + FactSheetMsg FactSheetMsg { get; set; } + FactsheetExtendMsg FactsheetExtendMsg { get; set; } + NavigationPathEdge[] BasePath { get; } + NavigationPathEdge[] FullPath { get; } + string[] CurrentZones { get; } + void Log(string message, LogLevel level = LogLevel.Information); + Task> InstantAction(RobotNet.RobotShares.VDA5050.InstantAction.Action action, bool waittingFisished); + Task MoveToNode(string goalName, IDictionary>? actions = null, double? lastAngle = null); + Task MoveRandom(List nodes); + Task CancelOrder(); + Task CancelAction(); + void Initialize(double x, double y, double theta); + MessageResult Rotate(double angle); + MessageResult MoveStraight(double x, double y); + void Dispose(); +} diff --git a/RobotNet.RobotManager/Services/IRobotOrder.cs b/RobotNet.RobotManager/Services/IRobotOrder.cs new file mode 100644 index 0000000..8254af5 --- /dev/null +++ b/RobotNet.RobotManager/Services/IRobotOrder.cs @@ -0,0 +1,15 @@ +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services; + +public interface IRobotOrder +{ + bool IsError { get; } + bool IsCompleted { get; } + bool IsProcessing { get; } + bool IsCanceled { get; } + string[] Errors { get; } + NavigationPathEdge[] FullPath { get; } + NavigationPathEdge[] BasePath { get; } + void CreateComledted(); +} diff --git a/RobotNet.RobotManager/Services/JsonOptionExtends.cs b/RobotNet.RobotManager/Services/JsonOptionExtends.cs new file mode 100644 index 0000000..de15515 --- /dev/null +++ b/RobotNet.RobotManager/Services/JsonOptionExtends.cs @@ -0,0 +1,18 @@ +using System.Text.Json; + +namespace RobotNet.RobotManager.Services; + +public class JsonOptionExtends +{ + public static readonly JsonSerializerOptions Read = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; + + public static readonly JsonSerializerOptions Write = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + }; +} diff --git a/RobotNet.RobotManager/Services/LoggerController.cs b/RobotNet.RobotManager/Services/LoggerController.cs new file mode 100644 index 0000000..f0c31f0 --- /dev/null +++ b/RobotNet.RobotManager/Services/LoggerController.cs @@ -0,0 +1,108 @@ +namespace RobotNet.RobotManager.Services; + +public class LoggerController(ILogger Logger) where T : class +{ + public event Action? LoggerUpdate; + public void Write(string message, LogLevel level) + { + switch (level) + { + case LogLevel.Trace: + Logger.LogTrace("{mes}", message); + break; + case LogLevel.Debug: + Logger.LogDebug("{mes}", message); + break; + case LogLevel.Information: + Logger.LogInformation("{mes}", message); + break; + case LogLevel.Warning: + Logger.LogWarning("{mes}", message); + break; + case LogLevel.Error: + Logger.LogError("{mes}", message); + break; + case LogLevel.Critical: + Logger.LogCritical("{mes}", message); + break; + } + LoggerUpdate?.Invoke(); + } + + public void Write(string message) + { + Write(message, LogLevel.Information); + } + + public async Task WriteAsync(string message) + { + var write = Task.Run(() => Write(message)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task TraceAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Trace)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task DebugAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Debug)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task InfoAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Information)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task WarningAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Warning)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task ErrorAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Error)); + await write.WaitAsync(CancellationToken.None); + } + + public async Task CriticalAsync(string message) + { + var write = Task.Run(() => Write(message, LogLevel.Critical)); + await write.WaitAsync(CancellationToken.None); + } + + public void Trace(string message) + { + Write(message, LogLevel.Trace); + } + + public void Debug(string message) + { + Write(message, LogLevel.Debug); + } + + public void Info(string message) + { + Write(message, LogLevel.Information); + } + + public void Warning(string message) + { + Write(message, LogLevel.Warning); + } + + public void Error(string message) + { + Write(message, LogLevel.Error); + } + + public void Critical(string message) + { + Write(message, LogLevel.Critical); + } +} diff --git a/RobotNet.RobotManager/Services/MapManager.cs b/RobotNet.RobotManager/Services/MapManager.cs new file mode 100644 index 0000000..6334b63 --- /dev/null +++ b/RobotNet.RobotManager/Services/MapManager.cs @@ -0,0 +1,267 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.HubClients; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services; + +public class MapManager(IServiceProvider ServiceProvider, LoggerController Logger) : BackgroundService +{ + private MapHubClient? MapHub; + private readonly Dictionary MapData = []; + + public async Task> GetMapData(Guid mapId) + { + try + { + if (MapData.TryGetValue(mapId, out MapDataDto? mapData) && mapData is not null) + { + if (!mapData.Active) return new(false, "Bản đồ chưa được active"); + return new(true) { Data = mapData }; + } + if (MapHub is null || !MapHub.IsConnected) + return new(false, "Chưa khởi tạo kết nối với quản lí bản đồ"); + var mapDataResult = await MapHub.GetMapData(mapId); + if (mapDataResult is not null && mapDataResult.Data is not null) + { + if (!mapDataResult.IsSuccess) return new(false, mapDataResult.Message); + MapData[mapId] = mapDataResult.Data; + return new(true) { Data = mapDataResult.Data }; + } + return new(false, "Không thể lấy dữ liệu bản đồ"); + } + catch (Exception ex) + { + Logger.Warning($"GetMapData: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + return new(false, $"GetMapData: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + } + } + + public async Task> GetMapInfo(Guid mapId) + { + try + { + if (MapHub is null || !MapHub.IsConnected) + return new(false, "Chưa khởi tạo kết nối với quản lí bản đồ"); + var mapInfoResult =await MapHub.GetMapInfoById(mapId); + if (mapInfoResult is not null && mapInfoResult.Data is not null) + { + if (!mapInfoResult.IsSuccess) return new(false, mapInfoResult.Message); + return new(true) { Data = mapInfoResult.Data }; + } + return new(false, "Không thể lấy dữ liệu bản đồ"); + } + catch (Exception ex) + { + Logger.Warning($"GetMapInfo: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + return new(false, $"GetMapInfo: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + } + } + + public async Task> GetMapInfo(string mapName) + { + try + { + if (MapHub is null || !MapHub.IsConnected) + return new(false, "Chưa khởi tạo kết nối với quản lí bản đồ"); + var mapInfoResult = await MapHub.GetMapInfoByName(mapName); + if (mapInfoResult is not null && mapInfoResult.Data is not null) + { + if (!mapInfoResult.IsSuccess) return new(false, mapInfoResult.Message); + return new(true) { Data = mapInfoResult.Data }; + } + return new(false, "Không thể lấy dữ liệu bản đồ"); + } + catch (Exception ex) + { + Logger.Warning($"GetMapInfo: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + return new(false, $"GetMapInfo: Lấy dữ liệu bản đồ có lỗi xảy ra: {ex.Message}"); + } + } + + public NodeDto[] GetNegativeNodes(Guid mapId, Guid nodeId) + { + var mapData = Task.Run(async () => await GetMapData(mapId)); + mapData.Wait(); + if (mapData is null || !mapData.Result.IsSuccess || mapData.Result.Data is null) return []; + var map = mapData.Result.Data; + var ListNodesNegative = new List(); + var ListPaths = map.Edges.Where(p => p.EndNodeId == nodeId || p.StartNodeId == nodeId); + foreach (var path in ListPaths) + { + if (path.StartNodeId == nodeId && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Forward)) + { + var nodeAdd = map.Nodes.FirstOrDefault(p => p.Id == path.EndNodeId); + if (nodeAdd is not null) ListNodesNegative.Add(nodeAdd); + continue; + } + if (path.EndNodeId == nodeId && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Backward)) + { + var nodeAdd = map.Nodes.FirstOrDefault(p => p.Id == path.StartNodeId); + if (nodeAdd is not null) ListNodesNegative.Add(nodeAdd); + continue; + } + } + return [.. ListNodesNegative]; + } + + public EdgeDto[] GetEdges(Guid mapId, NodeDto[] nodes) + { + if (nodes.Length < 2) return []; + if (MapData.TryGetValue(mapId, out MapDataDto? mapData) && mapData is not null) + { + List edges = []; + for (int i = 0; i < nodes.Length - 1; i++) + { + var edge = mapData.Edges.FirstOrDefault(e => e.StartNodeId == nodes[i].Id && e.EndNodeId == nodes[i + 1].Id || + e.EndNodeId == nodes[i].Id && e.StartNodeId == nodes[i + 1].Id); + if (edge is null) + { + if (i != 0) return []; + edges.Add(new EdgeDto() + { + Id = Guid.NewGuid(), + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + }); + continue; + } + bool isReverse = nodes[i].Id != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; + edges.Add(new() + { + Id = edge.Id, + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }); + } + return [.. edges]; + } + return []; + } + + public EdgeDto? GetEdge(Guid startNodeId, Guid endNodeId, Guid mapId) + { + if (MapData.TryGetValue(mapId, out MapDataDto? mapData) && mapData is not null) + { + var edge = mapData.Edges.FirstOrDefault(e => (e.StartNodeId == startNodeId && e.EndNodeId == endNodeId) || (e.StartNodeId == endNodeId && e.EndNodeId == startNodeId)); + if (edge is not null) + { + bool isReverse = startNodeId != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; + return new EdgeDto + { + Id = edge.Id, + StartNodeId = startNodeId, + EndNodeId = endNodeId, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }; + } + } + return null; + } + + public NodeDto[][] GetNegativePaths(Guid mapId, NodeDto node, double distance) + { + var negativePaths = new Dictionary(); + var currentPath = new List { node }; + var visitedNodes = new Dictionary { { node.Id, 0 } }; + + void DFS(NodeDto currentNode, double currentDistance, NodeDto[] path) + { + var negatvieNodes = GetNegativeNodes(mapId, currentNode.Id); + foreach (var negativeNode in negatvieNodes) + { + if (IsHasElement(mapId, negativeNode.Id)) continue; + NodeDto[] newPath = [.. path, negativeNode]; + var newDistance = currentDistance + Math.Sqrt(Math.Pow(currentNode.X - negativeNode.X, 2) + Math.Pow(currentNode.Y - negativeNode.Y, 2)); + if (visitedNodes.TryGetValue(negativeNode.Id, out double oldDistance)) + { + if (oldDistance > newDistance) + { + visitedNodes.Remove(negativeNode.Id); + visitedNodes.Add(negativeNode.Id, newDistance); + negativePaths.Remove(negativeNode.Id); + if (newDistance >= distance) negativePaths.Add(negativeNode.Id, [.. newPath]); + else DFS(negativeNode, newDistance, newPath); + } + } + else + { + visitedNodes.Add(negativeNode.Id, newDistance); + if (newDistance >= distance) negativePaths.Add(negativeNode.Id, [.. newPath]); + else DFS(negativeNode, newDistance, newPath); + } + } + } + DFS(node, 0, [.. currentPath]); + //foreach (var key in negativePaths.Keys.ToList()) + //{ + // var path = negativePaths[key]; + // if (path != null && path.Length > 0) negativePaths[key] = [..path.Skip(1)]; + // else negativePaths.Remove(key); + //} + return [.. negativePaths.Values]; + } + + private bool IsHasElement(Guid mapId, Guid nodeId) + { + var mapData = Task.Run(async () => await GetMapData(mapId)); + mapData.Wait(); + if (mapData is null || !mapData.Result.IsSuccess || mapData.Result.Data is null) return false; + var map = mapData.Result.Data; + return map.Elements.Any(e => e.NodeId == nodeId); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + using var scope = ServiceProvider.CreateAsyncScope(); + MapHub = scope.ServiceProvider.GetRequiredService(); + + MapHub.MapUpdated += MapHub_Updated; + await MapHub.StartAsync(); + break; + } + + catch (Exception ex) + { + Logger.Warning($"Map Manager Execute: Khởi tạo kết nối với MapManager có lỗi xảy ra: {ex.Message}"); + await Task.Delay(2000, stoppingToken); + } + } + } + + private void MapHub_Updated(Guid mapId) + { + Logger.Info($"Map update active: {mapId}"); + if (MapHub is null || !MapHub.IsConnected) return; + var mapDataResult = MapHub.GetMapData(mapId).Result; + if (mapDataResult is not null && mapDataResult.Data is not null) + { + if (!mapDataResult.IsSuccess) return; + MapData[mapId] = mapDataResult.Data; + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + if (MapHub is not null && MapHub.IsConnected) await MapHub.StopAsync(); + await base.StopAsync(cancellationToken); + } +} diff --git a/RobotNet.RobotManager/Services/MapManagerAccessTokenHandler.cs b/RobotNet.RobotManager/Services/MapManagerAccessTokenHandler.cs new file mode 100644 index 0000000..46b1b4e --- /dev/null +++ b/RobotNet.RobotManager/Services/MapManagerAccessTokenHandler.cs @@ -0,0 +1,33 @@ +using OpenIddict.Client; +using RobotNet.OpenIddictClient; +using System.Net.Http.Headers; + +namespace RobotNet.RobotManager.Services; + +public class MapManagerAccessTokenHandler(OpenIddictClientService openIddictClient, IConfiguration configuration) : DelegatingHandler +{ + private readonly OpenIddictResourceOptions options = configuration.GetSection("MapManager").Get() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + private string? CachedToken; + private DateTime TokenExpiry; + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(CachedToken) || DateTime.UtcNow >= TokenExpiry) + { + var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new() + { + Scopes = [.. options.Scopes], + }); + + if (result != null && !string.IsNullOrEmpty(result.AccessToken) && result.AccessTokenExpirationDate != null) + { + TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime; + CachedToken = result.AccessToken; + } + } + + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", CachedToken); + + return await base.SendAsync(request, cancellationToken); + } +} diff --git a/RobotNet.RobotManager/Services/MqttBroker.cs b/RobotNet.RobotManager/Services/MqttBroker.cs new file mode 100644 index 0000000..338c2bf --- /dev/null +++ b/RobotNet.RobotManager/Services/MqttBroker.cs @@ -0,0 +1,119 @@ +using MQTTnet; +using MQTTnet.Protocol; +using MQTTnet.Server; +using RobotNet.RobotManager.Data; +using RobotNet.RobotShares.VDA5050; +using RobotNet.RobotShares.VDA5050.Connection; +using System.Text.Json; + +namespace RobotNet.RobotManager.Services; + +public class MqttBroker : IHostedService +{ + private readonly VDA5050Setting VDA5050Setting = new(); + + public event Action? NewClientConnected; + public event Action? NewClientDisconnected; + public uint ConnectionHeaderId = 0; + + private MqttServer? MqttServer; + private readonly IServiceProvider ServiceProvider; + + public MqttBroker(IServiceProvider serviceProvider, IConfiguration configuration) + { + configuration.Bind("VDA5050Setting", VDA5050Setting); + ServiceProvider = serviceProvider; + } + + public MqttConnectReasonCode ValidatingConnection(ValidatingConnectionEventArgs arg) + { + using var scope = ServiceProvider.CreateScope(); + var RobotDb = scope.ServiceProvider.GetRequiredService(); + if (!RobotDb.Robots.Any(robot => arg.ClientId.ToLower() == robot.RobotId.ToString().ToLower()) && arg.ClientId != "FleetManager" && arg.ClientId != "OpenACS") + return MqttConnectReasonCode.ClientIdentifierNotValid; + if (!arg.UserName.Equals(VDA5050Setting.UserName, StringComparison.Ordinal) || !arg.Password.Equals(VDA5050Setting.Password, StringComparison.Ordinal)) + return MqttConnectReasonCode.BadUserNameOrPassword; + return MqttConnectReasonCode.Success; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (!VDA5050Setting.ServerEnable) return; + while (!cancellationToken.IsCancellationRequested) + { + try + { + var mqttFactory = new MqttServerFactory(); + + var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint() + .WithDefaultCommunicationTimeout(TimeSpan.FromSeconds(VDA5050Setting.ConnectionTimeoutSeconds)) + .WithTcpKeepAliveInterval(VDA5050Setting.KeepAliveInterval) + .WithConnectionBacklog(VDA5050Setting.ConnectionBacklog) + .Build(); + + MqttServer = mqttFactory.CreateMqttServer(mqttServerOptions); + MqttServer.ValidatingConnectionAsync += e => + { + var validate = ValidatingConnection(e); + if (validate != MqttConnectReasonCode.Success) e.ReasonCode = validate; + + return Task.CompletedTask; + }; + + var connection = new ConnectionMsg() + { + Manufacturer = VDA5050Setting.Manufacturer, + Version = VDA5050Setting.Version, + }; + + MqttServer.ClientConnectedAsync += e => + { + connection.HeaderId = ++ConnectionHeaderId; + connection.SerialNumber = e.ClientId; + connection.Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + connection.ConnectionState = ConnectionState.ONLINE.ToString(); + var payload = JsonSerializer.Serialize(connection); + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("connection") + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + MqttServer.InjectApplicationMessage(new(applicationMessage)); + NewClientConnected?.Invoke(e.ClientId); + return Task.CompletedTask; + }; + + MqttServer.ClientDisconnectedAsync += e => + { + connection.HeaderId = ++ConnectionHeaderId; + connection.SerialNumber = e.ClientId; + connection.Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + connection.ConnectionState = ConnectionState.OFFLINE.ToString(); + + var payload = JsonSerializer.Serialize(connection); + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic("connection") + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + MqttServer.InjectApplicationMessage(new(applicationMessage)); + NewClientDisconnected?.Invoke(e.ClientId); + return Task.CompletedTask; + }; + + await MqttServer.StartAsync(); + return; + } + catch + { + await Task.Delay(1000, cancellationToken); + continue; + } + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await MqttServer.StopAsync(); + } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/ACSHeader.cs b/RobotNet.RobotManager/Services/OpenACS/ACSHeader.cs new file mode 100644 index 0000000..4dff75c --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/ACSHeader.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class ACSHeader(string messageName, string time) +{ + [JsonPropertyName("msgname")] + [Required] + public string MessageName { get; set; } = messageName; + + [JsonPropertyName("time")] + [Required] + public string Time { get; set; } = time; + +} diff --git a/RobotNet.RobotManager/Services/OpenACS/ACSStatusResponse.cs b/RobotNet.RobotManager/Services/OpenACS/ACSStatusResponse.cs new file mode 100644 index 0000000..9e100cf --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/ACSStatusResponse.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class ACSStatusBodyResponse +{ + [JsonPropertyName("result")] + [Required] + public string Result { get; set; } = string.Empty; + +} + +public class ACSStatusResponse +{ + [JsonPropertyName("header")] + [Required] + public ACSHeader Header { get; set; } = new("", DateTime.MinValue.ToString("yyyy-MM-dd HH:mm:ss.fff")); + + + [JsonPropertyName("body")] + [Required] + public ACSStatusBodyResponse Body { get; set; } = new(); +} diff --git a/RobotNet.RobotManager/Services/OpenACS/AGVLocation.cs b/RobotNet.RobotManager/Services/OpenACS/AGVLocation.cs new file mode 100644 index 0000000..bbe3a6c --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/AGVLocation.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class AGVLocation +{ + [JsonPropertyName("world_x")] + [Required] + public string X { get; set; } = "0"; + + [JsonPropertyName("world_y")] + [Required] + public string Y { get; set; } = "0"; + + [JsonPropertyName("world_z")] + [Required] + public string Z { get; set; } = "0"; + + [JsonPropertyName("direction")] + [Required] + public string Direction { get; set; } = "0"; +} diff --git a/RobotNet.RobotManager/Services/OpenACS/AGVState.cs b/RobotNet.RobotManager/Services/OpenACS/AGVState.cs new file mode 100644 index 0000000..7e61706 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/AGVState.cs @@ -0,0 +1,15 @@ +namespace RobotNet.RobotManager.Services.OpenACS; + +public enum AGVState +{ + Offline = -1, + Error = 0, + Idle = 1, + Processing = 2, + Pause = 3, + DockingFail = 4, + NoPose = 5, + Charging = 6, + Run = 7, + Stop = 8, +} diff --git a/RobotNet.RobotManager/Services/OpenACS/OpenACSException.cs b/RobotNet.RobotManager/Services/OpenACS/OpenACSException.cs new file mode 100644 index 0000000..5fd1607 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/OpenACSException.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotManager.Services.OpenACS; + +public class OpenACSException : Exception +{ + public OpenACSException() { } + public OpenACSException(string message) : base(message) { } + public OpenACSException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/OpenACSManager.cs b/RobotNet.RobotManager/Services/OpenACS/OpenACSManager.cs new file mode 100644 index 0000000..5513441 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/OpenACSManager.cs @@ -0,0 +1,102 @@ +using System.Text.Json; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class OpenACSManager : BackgroundService +{ + public bool TrafficEnable => Config.TrafficEnable; + public string TrafficURL => Config.TrafficURL ?? ""; + public string[] TrafficURLUsed => [..Config.TrafficURLUsed ?? []]; + public bool PublishEnable => Config.PublishEnable; + public string PublishURL => Config.PublishURL ?? ""; + public string[] PublishURLUsed => [..Config.PublishURLUsed ?? []]; + public int PublishInterval => Config.PublishInterval; + public event Action? PublishIntervalChanged; + + private ConfigData Config; + private const string DataPath = "openACSConfig.json"; + private struct ConfigData + { + public string PublishURL { get; set; } + public List PublishURLUsed { get; set; } + public bool PublishEnable { get; set; } + public string TrafficURL { get; set; } + public List TrafficURLUsed { get; set; } + public bool TrafficEnable { get; set; } + public int PublishInterval { get; set; } + } + + public async Task UpdateTrafficURL(string url) + { + if (url == Config.TrafficURL) return; + + Config.TrafficURL = url; + Config.TrafficURLUsed ??= []; + var urlUsed = Config.TrafficURLUsed.FirstOrDefault(u => u.Equals(url, StringComparison.CurrentCultureIgnoreCase)); + if (urlUsed is not null) + { + Config.TrafficURLUsed.Remove(urlUsed); + } + else if (Config.PublishURLUsed.Count >= 10) Config.TrafficURLUsed.Remove(Config.TrafficURLUsed.Last()); + Config.TrafficURLUsed.Insert(0, url); + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + public async Task UpdateTrafficEnable(bool enable) + { + if (enable == Config.TrafficEnable) return; + + Config.TrafficEnable = enable; + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + public async Task UpdatePublishURL(string url) + { + if (url == Config.PublishURL) return; + + Config.PublishURL = url; + Config.PublishURLUsed ??= []; + var urlUsed = Config.PublishURLUsed.FirstOrDefault(u => u.Equals(url, StringComparison.OrdinalIgnoreCase)); + if (urlUsed is not null) + { + Config.PublishURLUsed.Remove(urlUsed); + } + else if (Config.PublishURLUsed.Count >= 10) Config.PublishURLUsed.Remove(Config.PublishURLUsed.Last()); + Config.PublishURLUsed.Insert(0, url); + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + public async Task UpdatePublishEnable(bool enable) + { + if (enable == Config.PublishEnable) return; + + Config.PublishEnable = enable; + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + public async Task UpdatePublishInterval(int interval) + { + if (interval == Config.PublishInterval) return; + + Config.PublishInterval = interval; + PublishIntervalChanged?.Invoke(); + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (File.Exists(DataPath)) + { + try + { + Config = JsonSerializer.Deserialize(await File.ReadAllTextAsync(DataPath, CancellationToken.None)); + PublishIntervalChanged?.Invoke(); + } + catch (JsonException) + { + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None); + } + } + else await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None); + } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/OpenACSPublisher.cs b/RobotNet.RobotManager/Services/OpenACS/OpenACSPublisher.cs new file mode 100644 index 0000000..e6d1e61 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/OpenACSPublisher.cs @@ -0,0 +1,171 @@ +using RobotNet.RobotShares.VDA5050.State; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class OpenACSPublisher(IConfiguration configuration, + LoggerController Logger, + RobotManager RobotManager, + OpenACSManager OpenACSManager) : BackgroundService +{ + public int PublishCount { get; private set; } + private WatchTimerAsync? Timer; + private readonly string ACSSiteCode = configuration["ACSStatusConfig:SiteCode"] ?? "VN03"; + private readonly string ACSAreaCode = configuration["ACSStatusConfig:AreaCode"] ?? "DA3_FL1"; + private readonly string ACSAreaName = configuration["ACSStatusConfig:AreaName"] ?? "DA3_WM"; + private readonly double ACSExtendX = configuration.GetValue("ACSStatusConfig:ExtendX"); + private readonly double ACSExtendY = configuration.GetValue("ACSStatusConfig:ExtendY"); + private readonly double ACSExtendTheta = configuration.GetValue("ACSStatusConfig:ExtendTheta"); + + private async Task TimerHandler() + { + if (OpenACSManager.PublishEnable && !string.IsNullOrEmpty(OpenACSManager.PublishURL)) + { + try + { + using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }; + var robotIds = RobotManager.RobotSerialNumbers.ToArray(); + foreach (var robotId in robotIds) + { + var startTime = DateTime.Now; + var robot = RobotManager[robotId]; + if (robot == null || robot.StateMsg == null || robot.StateMsg.AgvPosition == null) continue; + if (!DateTime.TryParse(robot.StateMsg.Timestamp, out DateTime lastTimeUpdate)) continue; + if ((startTime - lastTimeUpdate).TotalMilliseconds > OpenACSManager.PublishInterval) continue; + + int batLevel = (int)robot.StateMsg.BatteryState.BatteryHealth; + if (batLevel <= 0) batLevel = 85; + + int batVol = (int)robot.StateMsg.BatteryState.BatteryVoltage; + if (batVol <= 0) batVol = 24; + var status = new RobotPublishStatusV2() + { + Header = new("AGV_STATUS", lastTimeUpdate.ToString("yyyy-MM-dd HH:mm:ss.fff")), + Body = new() + { + Id = robot.SerialNumber, + Location = new() + { + X = (robot.StateMsg.AgvPosition.X + ACSExtendX).ToString(), + Y = (robot.StateMsg.AgvPosition.Y + ACSExtendY).ToString(), + Z = "0", + Direction = (robot.StateMsg.AgvPosition.Theta + ACSExtendTheta).ToString(), + }, + SiteCode = ACSSiteCode, + AreaCode = ACSAreaCode, + AreaName = ACSAreaName, + MarkerId = string.IsNullOrEmpty(robot.StateMsg.LastNodeId) ? null : robot.StateMsg.LastNodeId, + BatteryId = null, + BatteryLevel = batLevel.ToString(), + BatteryVoltage = batVol.ToString(), + BatterySOH = null, + BatteryCurrent = "1.0", + BatteryTemprature = "30", + StationId = null, + Loading = robot.StateMsg.Loads.Length != 0 ? "1" : "0", + ErrorCode = GetErrorCode(robot.StateMsg.Errors ?? [])?.ToString() ?? null, + State = GetStatus(robot.StateMsg).ToString(), + } + }; + + var response = await HttpClient.PostAsJsonAsync(OpenACSManager.PublishURL, status); + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(); + if (result == null) + { + Logger.Error("Failed to convert response.Content to ACSStatusResponse"); + } + else if (result.Header.MessageName == "AGV_STATUS_ACK" && result.Body.Result == "OK") + { + PublishCount++; + } + else + { + Logger.Warning($"ACS response is not OK: {System.Text.Json.JsonSerializer.Serialize(result)}"); + } + } + else + { + Logger.Warning($"Quá trình xuất bản tới {OpenACSManager.PublishURL} không thành công: {response.StatusCode}"); + } + } + } + catch (Exception ex) + { + Logger.Warning($"Quá trình xuất bản tới {OpenACSManager.PublishURL} có lỗi xảy ra: {ex.Message}"); + } + } + } + + private static int GetStatus(StateMsg state) + { + if (GetError(state) == ErrorLevel.FATAL || GetError(state) == ErrorLevel.WARNING) return (int)AGVState.Error; + else if (state.BatteryState.Charging) return (int)AGVState.Charging; + else if (state.Paused) return (int)AGVState.Pause; + else if (IsIdle(state)) return (int)AGVState.Idle; + else if (IsWorking(state)) return (int)AGVState.Run; + else return (int)AGVState.Stop; + } + + private static string? GetErrorCode(Error[] errors) + { + var error = errors.FirstOrDefault(); + if (error is not null && int.TryParse(error.ErrorType, out int errorCode)) return errorCode.ToString(); + return null; + } + + private static bool IsIdle(StateMsg state) + { + if (state.NodeStates.Length != 0 || state.EdgeStates.Length != 0) return false; + return true; + } + + private static bool IsWorking(StateMsg state) + { + if (state.NodeStates.Length != 0 || state.EdgeStates.Length != 0) return true; + return false; + } + + private static ErrorLevel GetError(StateMsg state) + { + if (state.Errors is not null) + { + if (state.Errors.Any(error => error.ErrorLevel == ErrorLevel.FATAL.ToString())) return ErrorLevel.FATAL; + if (state.Errors.Any(error => error.ErrorLevel == ErrorLevel.WARNING.ToString())) return ErrorLevel.WARNING; + } + return ErrorLevel.NONE; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + while (!stoppingToken.IsCancellationRequested) + { + try + { + OpenACSManager.PublishIntervalChanged += UpdateInterval; + Timer = new(OpenACSManager.PublishInterval <= 500 ? 500 : OpenACSManager.PublishInterval, TimerHandler, Logger); + Timer.Start(); + break; + } + catch (Exception ex) + { + Logger.Warning($"Publisher ACS: Quá trình khởi tạo có lỗi xảy ra: {ex.Message}"); + await Task.Delay(2000, stoppingToken); + } + } + } + + public void UpdateInterval() + { + Timer?.Dispose(); + Timer = new(OpenACSManager.PublishInterval <= 500 ? 500 : OpenACSManager.PublishInterval, TimerHandler, Logger); + Timer.Start(); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + Timer?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatus.cs b/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatus.cs new file mode 100644 index 0000000..7d9cc6f --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatus.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class RobotPublishStatusV2 +{ + [JsonPropertyName("header")] + [Required] + public ACSHeader Header { get; set; } = new("AGV_STATUS", DateTime.Today.ToString("yyyy-MM-dd HH:mm:ss.fff")); + + [JsonPropertyName("body")] + [Required] + public RobotPublishStatusBody Body { get; set; } = new(); +} \ No newline at end of file diff --git a/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatusBody.cs b/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatusBody.cs new file mode 100644 index 0000000..ada770b --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/RobotPublishStatusBody.cs @@ -0,0 +1,71 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class RobotPublishStatusBody +{ + [JsonPropertyName("agv_id")] + [Required] + public string Id { get; set; } = ""; + + [JsonPropertyName("state")] + [Required] + public string State { get; set; } = "-1"; + + [JsonPropertyName("site_code")] + [Required] + public string SiteCode { get; set; } = ""; + + [JsonPropertyName("area_code")] + [Required] + public string AreaCode { get; set; } = ""; + + [JsonPropertyName("area_name")] + [Required] + public string AreaName { get; set; } = ""; + + [JsonPropertyName("location")] + [Required] + public AGVLocation Location { get; set; } = new(); + + [JsonPropertyName("marker_id")] + [Required] + public string? MarkerId { get; set; } + + [JsonPropertyName("battery_level")] + [Required] + public string BatteryLevel { get; set; } = "0"; + + [JsonPropertyName("battery_voltage")] + [Required] + public string BatteryVoltage { get; set; } = "0"; + + [JsonPropertyName("battery_current")] + [Required] + public string BatteryCurrent { get; set; } = "0"; + + [JsonPropertyName("battery_temperature")] + [Required] + public string BatteryTemprature { get; set; } = "0"; + + [JsonPropertyName("battery_id")] + [Required] + public string? BatteryId { get; set; } + + [JsonPropertyName("battery_soh")] + [Required] + public string? BatterySOH { get; set; } = "0"; + + [JsonPropertyName("loading")] + [Required] + public string Loading { get; set; } = "0"; + + [JsonPropertyName("error_code")] + [Required] + public string? ErrorCode { get; set; } + + [JsonPropertyName("station_id")] + [Required] + public string? StationId { get; set; } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/TrafficACS.cs b/RobotNet.RobotManager/Services/OpenACS/TrafficACS.cs new file mode 100644 index 0000000..2dc5aa8 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/TrafficACS.cs @@ -0,0 +1,108 @@ +using NLog.Targets; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotShares.OpenACS; +using RobotNet.Shares; +using System.Text.Json; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class TrafficACS(OpenACSManager OpenACSManager, IConfiguration Configuration, LoggerController Logger) +{ + public bool Enable => OpenACSManager.TrafficEnable; + private readonly double TrafficCheckingDistanceMin = Configuration.GetValue("TrafficConfig:CheckingDistanceMin", 3); + public readonly double DeviationDistance = Configuration.GetValue("TrafficConfig:DeviationDistance", 0.5); + private static readonly JsonSerializerOptions jsonSerializeOptions = new() { + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + public async Task> RequestIn(string robotId, string zoneId) + { + try + { + if (!OpenACSManager.TrafficEnable) return new(true, "Kết nối với hệ thống traffic ACS không được bật") { Data = true }; + using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }; + + var model = new TrafficACSRequestV2(robotId, zoneId, TrafficRequestType.IN.ToInOutString()); + + var response = await (await HttpClient.PostAsJsonAsync(OpenACSManager.TrafficURL, model)).Content.ReadFromJsonAsync() ?? + throw new OpenACSException("Lỗi giao tiếp với hệ thống traffic ACS"); + if (response.Body.AgvId != robotId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS agv_id trả về {response.Body.AgvId} không trùng với dữ liệu gửi đi {robotId}"); + if (response.Body.TrafficZoneId != zoneId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS traffic_zone_id trả về {response.Body.TrafficZoneId} không trùng với dữ liệu gửi đi {zoneId}"); + if (response.Body.InOut != TrafficRequestType.IN.ToInOutString()) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS inout trả về {response.Body.InOut} không trùng với dữ liệu gửi đi in"); + Logger.Info($"{robotId} yêu cầu vào traffic zone {zoneId} \nRequest: {JsonSerializer.Serialize(model, jsonSerializeOptions)}\n trả về kết quả: {JsonSerializer.Serialize(response, jsonSerializeOptions)}"); + return new(true, "Yêu cầu vào traffic zone thành công") { Data = response.Body.Result == TrafficACSResult.GO }; + } + catch (OpenACSException ex) + { + Logger.Warning($"{robotId} request in xảy ra lỗi: {ex.Message}"); + return new(false, ex.Message); + } + catch (Exception ex) + { + Logger.Warning($"{robotId} request In xảy ra lỗi: {ex.Message}"); + return new(false, "Lỗi giao tiếp với hệ thống traffic ACS"); + } + } + + public async Task> RequestOut(string robotId, string zoneId) + { + try + { + if (!OpenACSManager.TrafficEnable) return new(true, "Kết nối với hệ thống traffic ACS không được bật") { Data = true}; + using var HttpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(15) }; + var model = new TrafficACSRequestV2(robotId, zoneId, TrafficRequestType.OUT.ToInOutString()); + var response = await (await HttpClient.PostAsJsonAsync(OpenACSManager.TrafficURL, model)).Content.ReadFromJsonAsync() ?? + throw new OpenACSException("Lỗi giao tiếp với hệ thống traffic ACS"); + if (response.Body.AgvId != robotId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS agv_id trả về {response.Body.AgvId} không trùng với dữ liệu gửi đi {robotId}"); + if (response.Body.TrafficZoneId != zoneId) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS traffic_zone_id trả về {response.Body.TrafficZoneId} không trùng với dữ liệu gửi đi {zoneId}"); + if (response.Body.InOut != TrafficRequestType.OUT.ToInOutString()) throw new OpenACSException($"Dữ liệu hệ thống traffic ACS inout trả về {response.Body.InOut} không trùng với dữ liệu gửi đi out"); + Logger.Info($"{robotId} yêu cầu ra khỏi traffic zone {zoneId} \nRequest: {JsonSerializer.Serialize(model, jsonSerializeOptions)}\n trả về kết quả: {JsonSerializer.Serialize(response, jsonSerializeOptions)}"); + return new(true, "Yêu cầu ra khỏi traffic zone thành công") { Data = response.Body.Result == TrafficACSResult.GO }; + } + catch (OpenACSException ex) + { + Logger.Warning($"{robotId} request out xảy ra lỗi: {ex.Message}"); + return new(false, ex.Message); + } + catch (Exception ex) + { + Logger.Warning($"{robotId} request Out xảy ra lỗi: {ex.Message}"); + return new(false, "Lỗi giao tiếp với hệ thống traffic ACS"); + } + } + + public Dictionary GetZones(Guid inNodeId, NodeDto[] nodes, Dictionary zones) + { + int inNodeIndex = Array.FindIndex(nodes, n => n.Id == inNodeId); + if (inNodeId == Guid.Empty || (inNodeIndex != -1 && inNodeIndex < nodes.Length - 1)) + { + List baseNodes = []; + List basezones = []; + double distance = 0; + int index = inNodeIndex != -1 ? inNodeIndex + 1 : 1; + for (; index < nodes.Length; index++) + { + baseNodes.Add(nodes[index]); + distance += Math.Sqrt(Math.Pow(nodes[index].X - nodes[index - 1].X, 2) + Math.Pow(nodes[index].Y - nodes[index - 1].Y, 2)); + if (distance > TrafficCheckingDistanceMin) break; + } + + Dictionary nodeZones = []; + + foreach (var node in baseNodes) + { + if (zones.TryGetValue(node.Id, out ZoneDto[]? zone) && zone is not null && zone.Length > 0) nodeZones.Add(node.Id, zone); + else nodeZones.Add(node.Id, []); + } + + return nodeZones; + } + else if (inNodeIndex == nodes.Length - 1) return []; + else + { + Logger.Warning($"Không tìm thấy node {inNodeId} trong danh sách nodes hoặc node này."); + return []; + } + } +} diff --git a/RobotNet.RobotManager/Services/OpenACS/TrafficACSRequest.cs b/RobotNet.RobotManager/Services/OpenACS/TrafficACSRequest.cs new file mode 100644 index 0000000..8b3e9f4 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/TrafficACSRequest.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; + +public class TrafficACSRequestBody(string agvId, string trafficZoneId, string inOut) +{ + [JsonPropertyName("agv_id")] + [Required] + public string AgvId { get; set; } = agvId; + + [JsonPropertyName("traffic_zone_id")] + [Required] + public string TrafficZoneId { get; set; } = trafficZoneId; + + [JsonPropertyName("inout")] + [Required] + public string InOut { get; set; } = inOut; +} + +public class TrafficACSRequestV2 +{ + [JsonPropertyName("header")] + [Required] + public ACSHeader Header { get; set; } = new("TRAFFIC_REQ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); + + [JsonPropertyName("body")] + [Required] + public TrafficACSRequestBody Body { get; set; } + + public TrafficACSRequestV2(string agvId, string trafficZoneId, string inOut) + { + if (string.IsNullOrWhiteSpace(agvId)) + throw new ArgumentException("AGV ID không thể rỗng.", nameof(agvId)); + if (string.IsNullOrWhiteSpace(trafficZoneId)) + throw new ArgumentException("Traffic Zone ID không thể rỗng.", nameof(trafficZoneId)); + if (string.IsNullOrWhiteSpace(inOut)) + throw new ArgumentException("In OUT không thể rỗng.", nameof(inOut)); + + Body = new (agvId, trafficZoneId, inOut); + } +} \ No newline at end of file diff --git a/RobotNet.RobotManager/Services/OpenACS/TrafficACSResponse.cs b/RobotNet.RobotManager/Services/OpenACS/TrafficACSResponse.cs new file mode 100644 index 0000000..d3af345 --- /dev/null +++ b/RobotNet.RobotManager/Services/OpenACS/TrafficACSResponse.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace RobotNet.RobotManager.Services.OpenACS; +public class TrafficACSResult +{ + public static string GO => "go"; + public static string NO => "no"; +} +public class TrafficACSResponse +{ + [JsonPropertyName("time")] + public string Time { get; set; } = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + [JsonPropertyName("agv_id")] + public string AgvId { get; set; } = string.Empty; + + [JsonPropertyName("traffic_zone_id")] + [Required] + public string TrafficZoneId { get; set; } = string.Empty; + + [JsonPropertyName("inout")] + [Required] + public string InOut { get; set; } = string.Empty; + + [JsonPropertyName("result")] + [Required] + public string Result { get; set; } = string.Empty; +} + + +public class TrafficACSResponseBody +{ + + [JsonPropertyName("agv_id")] + [Required] + public string AgvId { get; set; } = string.Empty; + + [JsonPropertyName("traffic_zone_id")] + [Required] + public string TrafficZoneId { get; set; } = string.Empty; + + [JsonPropertyName("inout")] + [Required] + public string InOut { get; set; } = string.Empty; + + [JsonPropertyName("result")] + [Required] + public string Result { get; set; } = string.Empty; +} + +public class TrafficACSResponseV2 +{ + + [JsonPropertyName("header")] + [Required] + public ACSHeader Header { get; set; } = new("TRAFFIC_RES", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); + + [JsonPropertyName("body")] + [Required] + public TrafficACSResponseBody Body { get; set; } = new(); +} \ No newline at end of file diff --git a/RobotNet.RobotManager/Services/PathPlanner.cs b/RobotNet.RobotManager/Services/PathPlanner.cs new file mode 100644 index 0000000..efcfe9e --- /dev/null +++ b/RobotNet.RobotManager/Services/PathPlanner.cs @@ -0,0 +1,351 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; +using System.Collections.Generic; + +namespace RobotNet.RobotManager.Services; + +public enum PathPlanningType +{ + None, + Angle, + StartDirection, + EndDirection, +} + +public class PathPlanner(IConfiguration Configuration, MapManager MapManager) +{ + private readonly double ResolutionSplit = Configuration.GetValue("PathPlanning:ResolutionSplit", 0.1); + private readonly PathPlanningType PathPlanningType = Configuration.GetValue("PathPlanning:Type", PathPlanningType.None); + private readonly RobotDirection StartDirection = Configuration.GetValue("PathPlanning:StartDirection", RobotDirection.NONE); + private readonly RobotDirection EndDirection = Configuration.GetValue("PathPlanning:EndDirection", RobotDirection.NONE); + + public async Task> Planning(double xRef, double yRef, double thetaRef, NavigationType type, Guid mapId, string nodeName) + { + var map = await MapManager.GetMapData(mapId); + if (!map.IsSuccess) return new(false, map.Message); + if (map.Data is null) return new(false, "Dữ liệu bản đồ có lỗi"); + + var goalNode = map.Data.Nodes.FirstOrDefault(n => n.Name == nodeName); + if (goalNode is null) return new(false, "Đích đến không tồn tại"); + + PathPlannerManager PathPlannerManager = new(); + var PathPlanner = PathPlannerManager.GetPathPlanningService(type); + PathPlanner.SetData(map.Data.Nodes, map.Data.Edges); + + if (type == NavigationType.Forklift) return PathPlanner.PathPlanningWithAngle(xRef, yRef, thetaRef, goalNode.Id); + else + { + return PathPlanningType switch + { + PathPlanningType.None => PathPlanner.PathPlanning(xRef, yRef, thetaRef, goalNode.Id), + PathPlanningType.Angle => PathPlanner.PathPlanningWithAngle(xRef, yRef, thetaRef, goalNode.Id), + PathPlanningType.StartDirection => PathPlanner.PathPlanningWithStartDirection(xRef, yRef, thetaRef, goalNode.Id, StartDirection), + PathPlanningType.EndDirection => PathPlanner.PathPlanningWithFinalDirection(xRef, yRef, thetaRef, goalNode.Id, EndDirection), + _ => PathPlanner.PathPlanning(xRef, yRef, thetaRef, goalNode.Id), + }; + } + } + + public async Task> Planning(Guid startNodeId, double thetaRef, NavigationType type, Guid mapId, Guid goalId) + { + var map = await MapManager.GetMapData(mapId); + if (!map.IsSuccess) return new(false, map.Message); + if (map.Data is null) return new(false, "Dữ liệu bản đồ có lỗi"); + + PathPlannerManager PathPlannerManager = new(); + var PathPlanner = PathPlannerManager.GetPathPlanningService(type); + PathPlanner.SetData(map.Data.Nodes, map.Data.Edges); + + if (type == NavigationType.Forklift) return PathPlanner.PathPlanningWithAngle(startNodeId, thetaRef, goalId); + else + { + return PathPlanningType switch + { + PathPlanningType.None => PathPlanner.PathPlanning(startNodeId, thetaRef, goalId), + PathPlanningType.Angle => PathPlanner.PathPlanningWithAngle(startNodeId, thetaRef, goalId), + PathPlanningType.StartDirection => PathPlanner.PathPlanningWithStartDirection(startNodeId, thetaRef, goalId, StartDirection), + PathPlanningType.EndDirection => PathPlanner.PathPlanningWithFinalDirection(startNodeId, thetaRef, goalId, EndDirection), + _ => PathPlanner.PathPlanning(startNodeId, thetaRef, goalId), + }; + } + } + + public static MessageResult PathSplit(NodeDto[] nodes, EdgeDto[] edges, double resolutionSplit) + { + if (nodes.Length < 1 || edges.Length == 0) return new(false, "Dữ liệu không hợp lệ"); + List pathSplit = []; + pathSplit.Add(new() + { + Id = nodes[0].Id, + X = nodes[0].X, + Y = nodes[0].Y, + Theta = nodes[0].Theta, + Direction = nodes[0].Direction, + Name = nodes[0].Name, + Actions = "CheckingNode" + }); + foreach (var edge in edges) + { + var startNode = nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) return new(false, "Dữ liệu lỗi: Điểm đầu cuối của edge không có trong danh sách nodes"); + NodeDto controlNode = new(); + + var EdgeCaculatorModel = new EdgeCaculatorModel() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }; + var length = MapEditorHelper.GetEdgeLength(EdgeCaculatorModel); + + if (length <= 0) continue; + double step = resolutionSplit / length; + + for (double t = step; t <= 1 - step; t += step) + { + (double x, double y) = MapEditorHelper.Curve(t, EdgeCaculatorModel); + pathSplit.Add(new() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + Theta = startNode.Theta, + Direction = startNode.Direction, + }); + } + + pathSplit.Add(new() + { + Id = endNode.Id, + X = endNode.X, + Y = endNode.Y, + Theta = endNode.Theta, + Direction = endNode.Direction, + Name = endNode.Name, + Actions = "CheckingNode" + }); + } + return new(true) { Data = [.. pathSplit] }; + } + + public MessageResult PathSplit(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 1 || edges.Length == 0) return new(false, "Dữ liệu không hợp lệ"); + List pathSplit = []; + pathSplit.Add(new() + { + Id = nodes[0].Id, + X = nodes[0].X, + Y = nodes[0].Y, + Theta = nodes[0].Theta, + Direction = nodes[0].Direction, + Name = nodes[0].Name, + Actions = "CheckingNode" + }); + foreach (var edge in edges) + { + var startNode = nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) return new(false, "Dữ liệu lỗi: Điểm đầu cuối của edge không có trong danh sách nodes"); + NodeDto controlNode = new(); + + var EdgeCaculatorModel = new EdgeCaculatorModel() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }; + var length = MapEditorHelper.GetEdgeLength(EdgeCaculatorModel); + + if (length <= 0) continue; + double step = ResolutionSplit / length; + + for (double t = step; t <= 1 - step; t += step) + { + (double x, double y) = MapEditorHelper.Curve(t, EdgeCaculatorModel); + pathSplit.Add(new() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + Theta = startNode.Theta, + Direction = startNode.Direction, + }); + } + + pathSplit.Add(new() + { + Id = endNode.Id, + X = endNode.X, + Y = endNode.Y, + Theta = endNode.Theta, + Direction = endNode.Direction, + Name = endNode.Name, + Actions = "CheckingNode" + }); + } + return new(true) { Data = [.. pathSplit] }; + } + + public static RobotDirection GetRobotStartDirection(NodeDto currentNode, NodeDto nearNode, EdgeDto edge, double robotInNodeAngle) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(currentNode, nearNode, edge, 0.1); + + var RobotNearNode = new NodeDto() + { + X = currentNode.X + Math.Cos(robotInNodeAngle * Math.PI / 180), + Y = currentNode.Y + Math.Sin(robotInNodeAngle * Math.PI / 180), + }; + return MapEditorHelper.GetAngle(currentNode, NearNode, RobotNearNode) > 89 ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + } + + public static RobotDirection[] GetRobotDirectionInPath(RobotDirection currentDirection, NodeDto[] nodes, EdgeDto[] edges) + { + RobotDirection[] RobotDirectionInNode = new RobotDirection[nodes.Length]; + if (nodes.Length > 0) RobotDirectionInNode[0] = currentDirection; + if (nodes.Length > 2) + { + for (int i = 1; i < nodes.Length - 1; i++) + { + NodeDto nearLastNode = MapEditorHelper.GetNearByNode(nodes[i], nodes[i - 1], edges[i - 1], 0.1); + NodeDto nearFutureNode = MapEditorHelper.GetNearByNode(nodes[i], nodes[i + 1], edges[i], 0.1); ; + var angle = MapEditorHelper.GetAngle(nodes[i], nearLastNode, nearFutureNode); + if (angle < 89) RobotDirectionInNode[i] = RobotDirectionInNode[i - 1] == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else RobotDirectionInNode[i] = RobotDirectionInNode[i - 1]; + } + } + if (nodes.Length > 1) RobotDirectionInNode[^1] = RobotDirectionInNode[^2]; + return RobotDirectionInNode; + } + + public static double[] GetRobotThetaInPath(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2 || edges.Length < 1 || nodes.Length - 1 != edges.Length) return []; + double[] RobotThetaInNode = new double[nodes.Length]; + if (nodes.Length > 1) + { + for (int i = 0; i < nodes.Length - 1; i++) + { + NodeDto nearFutureNode = MapEditorHelper.GetNearByNode(nodes[i], nodes[i + 1], edges[i], 0.1); + var angleForward = Math.Atan2(nearFutureNode.Y - nodes[i].Y, nearFutureNode.X - nodes[i].X) * 180 / Math.PI; + var angleBackward = Math.Atan2(nodes[i].Y - nearFutureNode.Y, nodes[i].X - nearFutureNode.X) * 180 / Math.PI; + + RobotThetaInNode[i] = nodes[i].Direction == Direction.FORWARD ? angleForward : angleBackward; + } + RobotThetaInNode[^1] = RobotThetaInNode[^2]; + } + return RobotThetaInNode; + } + + public static NodeDto[] CalculatorDirection(NodeDto[] nodes, EdgeDto[] edges) => CalculatorDirection(RobotDirection.FORWARD, nodes, edges); + + public static NodeDto[] CalculatorDirection(RobotDirection startDirection, NodeDto[] nodes, EdgeDto[] edges) + { + var directions = GetRobotDirectionInPath(startDirection, nodes, edges); + NodeDto[] returnNodes = [.. nodes]; + for (int i = 0; i < returnNodes.Length; i++) + { + returnNodes[i].Direction = MapCompute.GetNodeDirection(directions[i]); + } + var thetas = GetRobotThetaInPath(returnNodes, edges); + for (int i = 0; i < returnNodes.Length; i++) + { + returnNodes[i].Theta = thetas[i]; + } + return returnNodes; + } + + public static NodeDto[] CalculatorDirection(double theta, NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2) return nodes; + var RobotNearNode = new NodeDto() + { + X = nodes[0].X + Math.Cos(theta * Math.PI / 180), + Y = nodes[0].Y + Math.Sin(theta * Math.PI / 180), + }; + nodes[0].Direction = MapEditorHelper.GetAngle(nodes[0], RobotNearNode, nodes[1]) > 89 ? MapShares.Enums.Direction.BACKWARD : MapShares.Enums.Direction.FORWARD; + var directions = GetRobotDirectionInPath(MapCompute.GetRobotDirection(nodes[0].Direction), nodes, edges); + NodeDto[] returnNodes = [.. nodes]; + for (int i = 0; i < returnNodes.Length; i++) + { + returnNodes[i].Direction = MapCompute.GetNodeDirection(directions[i]); + } + var thetas = GetRobotThetaInPath(returnNodes, edges); + for (int i = 0; i < returnNodes.Length; i++) + { + returnNodes[i].Theta = thetas[i]; + } + return returnNodes; + } + + public static NodeDto[] CalculatorDirection(double theta, NodeDto[] nodes) + { + if (nodes.Length < 2) return nodes; + var RobotNearNode = new NodeDto() + { + X = nodes[0].X + Math.Cos(theta * Math.PI / 180), + Y = nodes[0].Y + Math.Sin(theta * Math.PI / 180), + }; + nodes[0].Direction = MapEditorHelper.GetAngle(nodes[0], RobotNearNode, nodes[1]) > 89 ? MapShares.Enums.Direction.BACKWARD : MapShares.Enums.Direction.FORWARD; + for (int i = 1; i < nodes.Length - 1; i++) + { + if (nodes[i].X == nodes[i - 1].X && nodes[i].Y == nodes[i - 1].Y) nodes[i].Direction = nodes[i - 1].Direction; + else if (nodes[i].X == nodes[i + 1].X && nodes[i].Y == nodes[i + 1].Y) nodes[i].Direction = nodes[i - 1].Direction; + else + { + var angle = MapEditorHelper.GetAngle(nodes[i], nodes[i - 1], nodes[i + 1]); + nodes[i].Direction = angle > 89 ? nodes[i - 1].Direction : (nodes[i - 1].Direction == MapShares.Enums.Direction.FORWARD ? MapShares.Enums.Direction.BACKWARD : MapShares.Enums.Direction.FORWARD); + } + } + nodes[^1].Direction = nodes[^2].Direction; + return nodes; + } + + public async Task>> GetZones(Guid mapId, NodeDto[] nodes) + { + var map = await MapManager.GetMapData(mapId); + if (!map.IsSuccess) return new(false, map.Message); + if (map.Data is null) return new(false, "Dữ liệu bản đồ có lỗi"); + + Dictionary NodeInZones = []; + foreach (var node in nodes) + { + List zones = []; + foreach(var zone in map.Data.Zones) + { + if (MapEditorHelper.IsPointInside(node.X, node.Y, zone)) zones.Add(zone); + } + if (zones.Count > 0) NodeInZones.Add(node.Id, [..zones]); + } + return new(true, "") { Data = NodeInZones }; + } + + public static ZoneDto[] GetZones(NodeDto[] nodes, Dictionary zones) + { + List returnZones = []; + foreach (var node in nodes) + { + if (zones.TryGetValue(node.Id, out ZoneDto[]? zone) && zone is not null && zone.Length > 0) + returnZones.AddRange(zone.Where(z => !returnZones.Any(zo => zo.Id == z.Id))); + } + return [.. returnZones]; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/AStar/AStarNode.cs b/RobotNet.RobotManager/Services/Planner/AStar/AStarNode.cs new file mode 100644 index 0000000..cab62bd --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/AStar/AStarNode.cs @@ -0,0 +1,28 @@ +namespace RobotNet.RobotManager.Services.Planner.AStar; + +#nullable disable + +public class AStarNode +{ + public Guid Id { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Cost { get; set; } + public double Heuristic { get; set; } + public double TotalCost => Cost + Heuristic; + public string Name { get; set; } + public AStarNode Parent { get; set; } + public List NegativeNodes { get; set; } = []; + + public override bool Equals(object obj) + { + if (obj is AStarNode other) + return Id == other.Id; + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Id); + } +} diff --git a/RobotNet.RobotManager/Services/Planner/AStar/AStarPathPlanner.cs b/RobotNet.RobotManager/Services/Planner/AStar/AStarPathPlanner.cs new file mode 100644 index 0000000..25e1c7c --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/AStar/AStarPathPlanner.cs @@ -0,0 +1,393 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.AStar; + +public class AStarPathPlanner(List Nodes, List Edges) +{ + public static NodeDto? GetCurveNode(double t, EdgeDto edge, NodeDto startNode, NodeDto endNode) + { + if (edge.TrajectoryDegree == TrajectoryDegree.Two) + { + return new() + { + X = (1 - t) * (1 - t) * startNode.X + 2 * t * (1 - t) * edge.ControlPoint1X + t * t * endNode.X, + Y = (1 - t) * (1 - t) * startNode.Y + 2 * t * (1 - t) * edge.ControlPoint1Y + t * t * endNode.Y + }; + } + else if (edge.TrajectoryDegree == TrajectoryDegree.Three) + { + return new() + { + X = Math.Pow(1 - t, 3) * startNode.X + 3 * Math.Pow(1 - t, 2) * t * edge.ControlPoint1X + 3 * Math.Pow(t, 2) * (1 - t) * edge.ControlPoint2X + Math.Pow(t, 3) * endNode.X, + Y = Math.Pow(1 - t, 3) * startNode.Y + 3 * Math.Pow(1 - t, 2) * t * edge.ControlPoint1Y + 3 * Math.Pow(t, 2) * (1 - t) * edge.ControlPoint2Y + Math.Pow(t, 3) * endNode.Y, + }; + } + return null; + } + + public static double DistanceToCurveEdge(NodeDto nodeRef, EdgeDto edge, NodeDto startNode, NodeDto endNode) + { + double dMin = Math.Sqrt(Math.Pow(nodeRef.X - startNode.X, 2) + Math.Pow(nodeRef.Y - startNode.Y, 2)); + var length = MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }); + double step = 0.1 / (length == 0 ? 0.1 : length); + + for (double t = 0; t <= 1; t += step) + { + var nodeCurve = GetCurveNode(t, edge, startNode, endNode); + if (nodeCurve is null) continue; + double d = Math.Sqrt(Math.Pow(nodeRef.X - nodeCurve.X, 2) + Math.Pow(nodeRef.Y - nodeCurve.Y, 2)); + if (d < dMin) dMin = d; + } + + return dMin; + } + + public static MessageResult DistanceToEdge(NodeDto nodeRef, EdgeDto edge, NodeDto startNode, NodeDto endNode) + { + + if (edge.TrajectoryDegree == TrajectoryDegree.One) + { + double time = 0; + var edgeLengthSquared = Math.Pow(startNode.X - endNode.X, 2) + Math.Pow(startNode.Y - endNode.Y, 2); + if (edgeLengthSquared > 0) + { + time = Math.Max(0, Math.Min(1, ((nodeRef.X - startNode.X) * (endNode.X - startNode.X) + (nodeRef.Y - startNode.Y) * (endNode.Y - startNode.Y)) / edgeLengthSquared)); + } + + double nearestX = startNode.X + time * (endNode.X - startNode.X); + double nearestY = startNode.Y + time * (endNode.Y - startNode.Y); + + return new(true) { Data = Math.Sqrt(Math.Pow(nodeRef.X - nearestX, 2) + Math.Pow(nodeRef.Y - nearestY, 2)) }; + } + else + { + return new(true) { Data = DistanceToCurveEdge(nodeRef, edge, startNode, endNode) }; + } + } + + public EdgeDto? GetClosesEdge(NodeDto nodeRef, double limitDistance, CancellationToken? cancellationToken = null) + { + double minDistance = double.MaxValue; + EdgeDto? edgeResult = null; + foreach (var edge in Edges) + { + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return null; + var startNode = Nodes.FirstOrDefault(node => node.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(node => node.Id == edge.EndNodeId); + if (startNode is null || endNode is null) continue; + + var getDistance = DistanceToEdge(nodeRef, edge, startNode, endNode); + if (getDistance.IsSuccess) + { + if (getDistance.Data < minDistance) + { + minDistance = getDistance.Data; + edgeResult = edge; + } + } + } + if (minDistance <= limitDistance) return edgeResult; + else return null; + } + + private NodeDto? GetOnNode(double x, double y, double limitDistance, CancellationToken? cancellationToken = null) + { + if (cancellationToken?.IsCancellationRequested == true) return null; + KDTree KDTree = new(Nodes); + return KDTree.FindNearest(x, y, limitDistance); + } + + private List GetNegativeNode(Guid nodeId, CancellationToken? cancellationToken = null) + { + var node = Nodes.FirstOrDefault(p => p.Id == nodeId); + if (node is null) return []; + + var ListNodesNegative = new List(); + var ListPaths = Edges.Where(p => p.EndNodeId == nodeId || p.StartNodeId == nodeId); + foreach (var path in ListPaths) + { + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return []; + if (path.StartNodeId == node.Id && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Forward)) + { + var nodeAdd = Nodes.FirstOrDefault(p => p.Id == path.EndNodeId); + if (nodeAdd is not null) ListNodesNegative.Add(nodeAdd); + continue; + } + if (path.EndNodeId == node.Id && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Backward)) + { + var nodeAdd = Nodes.FirstOrDefault(p => p.Id == path.StartNodeId); + if (nodeAdd is not null) ListNodesNegative.Add(nodeAdd); + continue; + } + } + return ListNodesNegative; + } + + private double GetNegativeCost(AStarNode currenNode, AStarNode negativeNode) + { + var negativeEdges = Edges.Where(e => e.StartNodeId == currenNode.Id && e.EndNodeId == negativeNode.Id || e.StartNodeId == negativeNode.Id && e.EndNodeId == currenNode.Id).ToList(); + double minDistance = double.MaxValue; + foreach (var edge in negativeEdges) + { + var startNode = Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) return 0; + var distance = MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }); + if (distance < minDistance) minDistance = distance; + } + return minDistance != double.MaxValue ? minDistance : 0; + } + + private List GetNegativeAStarNode(AStarNode nodeCurrent, NodeDto endNode, CancellationToken? cancellationToken = null) + { + var possiblePointNegative = new List(); + foreach (var nodeNegative in nodeCurrent.NegativeNodes) + { + nodeNegative.Parent = nodeCurrent; + var ListNodesNegative = GetNegativeNode(nodeNegative.Id, cancellationToken); + foreach (var item in ListNodesNegative) + { + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return []; + nodeNegative.NegativeNodes.Add(new AStarNode() + { + Id = item.Id, + X = item.X, + Y = item.Y, + Name = item.Name, + }); + } + var cost = GetNegativeCost(nodeCurrent, nodeNegative); + nodeNegative.Cost = (cost > 0 ? cost : Math.Sqrt(Math.Pow(nodeCurrent.X - nodeNegative.X, 2) + Math.Pow(nodeCurrent.Y - nodeNegative.Y, 2))) + nodeCurrent.Cost; + nodeNegative.Heuristic = Math.Abs(endNode.X - nodeNegative.X) + Math.Abs(endNode.Y - nodeNegative.Y); + possiblePointNegative.Add(nodeNegative); + } + return possiblePointNegative; + } + + private List Find(AStarNode startNode, NodeDto endNode, CancellationToken? cancellationToken = null) + { + try + { + var activeNodes = new PriorityQueue((a, b) => a.TotalCost.CompareTo(b.TotalCost)); + var visitedNodes = new HashSet(); + List Path = []; + activeNodes.Enqueue(startNode); + + while (activeNodes.Count != 0 && (!cancellationToken.HasValue || !cancellationToken.Value.IsCancellationRequested)) + { + var checkNode = activeNodes.Dequeue(); + if (checkNode.Id == endNode.Id) + { + var node = checkNode; + while (node != null) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + Path.Add(node); + node = node.Parent; + } + return Path; + } + + visitedNodes.Add(checkNode); + + var ListNodeNegative = GetNegativeAStarNode(checkNode, endNode, cancellationToken); + foreach (var node in ListNodeNegative) + { + if (visitedNodes.TryGetValue(node, out AStarNode? value) && value is not null) + { + if (value.TotalCost > node.TotalCost) + { + visitedNodes.Remove(value); + activeNodes.Enqueue(node); + } + continue; + } + + var activeNode = activeNodes.Items.FirstOrDefault(n => n.Id == node.Id); + if (activeNode is not null && activeNode.TotalCost > node.TotalCost) + { + activeNodes.Items.Remove(activeNode); + activeNodes.Enqueue(node); + } + else + { + activeNodes.Enqueue(node); + } + } + } + + return []; + } + catch + { + return []; + } + } + + public MessageResult> Planning(double x, double y, NodeDto goal, double maxDistanceToEdge, double maxDistanceToNode, CancellationToken? cancellationToken = null) + { + var Path = new List(); + + AStarNode RobotNode = new() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + Name = "RobotCurrentNode", + }; + var closesNode = GetOnNode(x, y, maxDistanceToNode); + if (closesNode is not null) + { + if (closesNode.Id == goal.Id) return new(true, "Robot đang đứng tại điểm đích") { Data = [goal] }; + RobotNode.Name = closesNode.Name; + RobotNode.Id = closesNode.Id; + RobotNode.X = closesNode.X; + RobotNode.Y = closesNode.Y; + + foreach (var negativeNode in GetNegativeNode(RobotNode.Id, cancellationToken)) + { + var cost = GetNegativeCost(RobotNode, new() { Id = negativeNode.Id, X = negativeNode.X, Y = negativeNode.Y }); + RobotNode.NegativeNodes.Add(new() + { + Id = negativeNode.Id, + X = negativeNode.X, + Y = negativeNode.Y, + Name = negativeNode.Name, + Cost = cost > 0 ? cost : Math.Sqrt(Math.Pow(RobotNode.X - negativeNode.X, 2) + Math.Pow(RobotNode.Y - negativeNode.Y, 2)), + Heuristic = Math.Abs(goal.X - negativeNode.X) + Math.Abs(goal.Y - negativeNode.Y), + }); + } + } + else + { + var closesEdge = GetClosesEdge(new() { X = x, Y = y }, maxDistanceToEdge, cancellationToken); + if (closesEdge is null) + { + return new(false, "Robot đang quá xa tuyến đường"); + } + + var startNodeForward = Nodes.FirstOrDefault(p => p.Id == closesEdge.StartNodeId); + var startNodeBackward = Nodes.FirstOrDefault(p => p.Id == closesEdge.EndNodeId); + if (startNodeForward is null || startNodeBackward is null) + { + return new(false, "Dữ liệu lỗi: điểm chặn của edge gần nhất không tồn tại"); + } + if (startNodeForward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Backward)) + { + Path.Add(startNodeBackward); + Path.Add(startNodeForward); + return new(true) { Data = Path }; + } + if (startNodeBackward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Forward)) + { + Path.Add(startNodeForward); + Path.Add(startNodeBackward); + return new(true) { Data = Path }; + } + if (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Backward) + { + RobotNode.NegativeNodes.Add(new() + { + Id = startNodeForward.Id, + X = startNodeForward.X, + Y = startNodeForward.Y, + Name = startNodeForward.Name, + Cost = Math.Sqrt(Math.Pow(RobotNode.X - startNodeForward.X, 2) + Math.Pow(RobotNode.Y - startNodeForward.Y, 2)), + Heuristic = Math.Abs(goal.X - startNodeForward.X) + Math.Abs(goal.Y - startNodeForward.Y), + }); + } + if (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Forward) + { + RobotNode.NegativeNodes.Add(new() + { + Id = startNodeBackward.Id, + X = startNodeBackward.X, + Y = startNodeBackward.Y, + Name = startNodeBackward.Name, + Cost = Math.Sqrt(Math.Pow(RobotNode.X - startNodeBackward.X, 2) + Math.Pow(RobotNode.Y - startNodeBackward.Y, 2)), + Heuristic = Math.Abs(goal.X - startNodeBackward.X) + Math.Abs(goal.Y - startNodeBackward.Y), + }); + } + } + + if (RobotNode.NegativeNodes.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + + var path = Find(RobotNode, goal, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + path.Reverse(); + foreach (var node in path) + { + if (node.Name == "RobotCurrentNode") + { + Path.Add(new() + { + Id = RobotNode.Id, + Name = RobotNode.Name, + X = RobotNode.X, + Y = RobotNode.Y, + }); + continue; + } + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + Path.Add(nodedb); + } + return new(true) { Data = Path }; + } + + public MessageResult Planning(NodeDto startNode, NodeDto goal, CancellationToken? cancellationToken = null) + { + var Path = new List(); + var currentNode = new AStarNode + { + Id = startNode.Id, + X = startNode.X, + Y = startNode.Y, + NegativeNodes = [..GetNegativeNode(startNode.Id).Select(n => new AStarNode + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + })], + }; + var path = Find(currentNode, goal, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + path.Reverse(); + foreach (var node in path) + { + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + Path.Add(nodedb); + } + return new(true) { Data = [.. Path] }; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/Differential/DifferentialPathPlanner.cs b/RobotNet.RobotManager/Services/Planner/Differential/DifferentialPathPlanner.cs new file mode 100644 index 0000000..d6b51a7 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Differential/DifferentialPathPlanner.cs @@ -0,0 +1,266 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.AStar; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.Differential; + +public class DifferentialPathPlanner : IPathPlanner +{ + private List Nodes = []; + private List Edges = []; + private const double ReverseDirectionAngleDegree = 89; + private const double Ratio = 0.1; + + private readonly PathPlanningOptions Options = new() + { + LimitDistanceToEdge = 1, + LimitDistanceToNode = 0.3, + ResolutionSplit = 0.1, + }; + + public void SetData(NodeDto[] nodes, EdgeDto[] edges) + { + Nodes = [.. nodes]; + Edges = [.. edges]; + } + + public void SetOptions(PathPlanningOptions options) + { + Options.LimitDistanceToNode = options.LimitDistanceToNode; + Options.LimitDistanceToEdge = options.LimitDistanceToEdge; + Options.ResolutionSplit = options.ResolutionSplit; + } + + public static RobotDirection[] GetRobotDirectionInPath(RobotDirection currentDirection, List nodeplanning, List edgePlanning) + { + RobotDirection[] RobotDirectionInNode = new RobotDirection[nodeplanning.Count]; + if (nodeplanning.Count > 0) RobotDirectionInNode[0] = currentDirection; + if (nodeplanning.Count > 2) + { + for (int i = 1; i < nodeplanning.Count - 1; i++) + { + NodeDto startNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i - 1], edgePlanning[i - 1], Ratio); + NodeDto endNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i + 1], edgePlanning[i], Ratio); ; + var angle = MapEditorHelper.GetAngle(nodeplanning[i], startNode, endNode); + if (angle < ReverseDirectionAngleDegree) RobotDirectionInNode[i] = RobotDirectionInNode[i - 1] == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else RobotDirectionInNode[i] = RobotDirectionInNode[i - 1]; + } + } + if (nodeplanning.Count > 1) RobotDirectionInNode[^1] = RobotDirectionInNode[^2]; + return RobotDirectionInNode; + } + + public static RobotDirection GetRobotDirection(NodeDto currentNode, NodeDto nearNode, EdgeDto edge, double robotInNodeAngle, bool isReverse) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(currentNode, nearNode, edge, Ratio); + + var RobotNearNode = new NodeDto() + { + X = currentNode.X + Math.Cos(robotInNodeAngle * Math.PI / 180), + Y = currentNode.Y + Math.Sin(robotInNodeAngle * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(currentNode, NearNode, RobotNearNode); + if (angle > ReverseDirectionAngleDegree) return isReverse ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else return isReverse ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + } + + private EdgeDto[] GetEdgesPlanning(NodeDto[] nodes) + { + var EdgesPlanning = new List(); + for (int i = 0; i < nodes.Length - 1; i++) + { + var edge = Edges.FirstOrDefault(e => e.StartNodeId == nodes[i].Id && e.EndNodeId == nodes[i + 1].Id || + e.EndNodeId == nodes[i].Id && e.StartNodeId == nodes[i + 1].Id); + if (edge is null) + { + if (i != 0) return []; + EdgesPlanning.Add(new EdgeDto() + { + Id = Guid.NewGuid(), + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + }); + continue; + } + bool isReverse = nodes[i].Id != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; + EdgesPlanning.Add(new() + { + Id = edge.Id, + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }); + } + return [.. EdgesPlanning]; + } + + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(x, + y, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y }, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}, {theta}]"); + if (Path.Data.Count == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([goal], []) }; + + var edgeplannings = GetEdgesPlanning([..Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, true); + var FinalDirection = GetRobotDirectionInPath(CurrenDirection, Path.Data, [..edgeplannings]); + foreach (var item in Path.Data) + { + item.Direction = MapCompute.GetNodeDirection(FinalDirection[Path.Data.IndexOf(item)]); + } + + return new(true) + { + Data = ([.. Path.Data], [.. edgeplannings]) + }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if(basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([goal], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, false); + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(double x, double y, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(startDiretion) == basicPath.Data.Nodes[0].Direction || startDiretion == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(double x, double y, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction || goalDirection == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(startNode, goal, cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Length < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + if (Path.Data.Length == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, true); + var FinalDirection = GetRobotDirectionInPath(CurrenDirection, [.. Path.Data], [.. edgeplannings]); + for (int i = 0; i < Path.Data.Length; i++) + { + Path.Data[i].Direction = MapCompute.GetNodeDirection(FinalDirection[i]); + } + + return new(true) + { + Data = ([.. Path.Data], [.. edgeplannings]) + }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, false); + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(startDiretion) == basicPath.Data.Nodes[0].Direction || startDiretion == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction || goalDirection == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/Fokrlift/ForkliftPathPlanner.cs b/RobotNet.RobotManager/Services/Planner/Fokrlift/ForkliftPathPlanner.cs new file mode 100644 index 0000000..b6fef0d --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Fokrlift/ForkliftPathPlanner.cs @@ -0,0 +1,596 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.AStar; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.Fokrlift; + +public class ForkliftPathPlanner : IPathPlanner +{ + private List Nodes = []; + private List Edges = []; + private const double ReverseDirectionAngleDegree = 89; + private const double RobotDirectionWithAngle = 90; + private const double Ratio = 0.1; + + private readonly PathPlanningOptions Options = new() + { + LimitDistanceToEdge = 1, + LimitDistanceToNode = 0.3, + ResolutionSplit = 0.1, + }; + + public void SetData(NodeDto[] nodes, EdgeDto[] edges) + { + Nodes = [.. nodes]; + Edges = [.. edges]; + } + + public void SetOptions(PathPlanningOptions options) + { + Options.LimitDistanceToNode = options.LimitDistanceToNode; + Options.LimitDistanceToEdge = options.LimitDistanceToEdge; + Options.ResolutionSplit = options.ResolutionSplit; + } + + private static bool TStructureExisted(List TStructures, NodeDto node1, NodeDto node2, NodeDto node3) + { + var TStructureExistedStep1 = TStructures.Where(ts => ts.Node1 == node1 || ts.Node2 == node1 || ts.Node3 == node1).ToList(); + if (TStructureExistedStep1.Count != 0) + { + var TStructureExistedStep2 = TStructureExistedStep1.Where(ts => ts.Node1 == node2 || ts.Node2 == node2 || ts.Node3 == node2).ToList(); + if (TStructureExistedStep2.Count != 0) + { + var TStructureExistedStep3 = TStructureExistedStep2.Where(ts => ts.Node1 == node3 || ts.Node2 == node3 || ts.Node3 == node3).ToList(); + if (TStructureExistedStep3.Count != 0) return true; + } + } + return false; + } + + private List GetTStructure() + { + List TStructures = []; + foreach (var node in Nodes) + { + var inEdges = Edges.Where(edge => edge.StartNodeId == node.Id || edge.EndNodeId == node.Id).ToList(); + if (inEdges.Count < 2) continue; + List inNodes = []; + foreach (var edge in inEdges) + { + var endNode = Nodes.FirstOrDefault(n => n.Id == (node.Id == edge.EndNodeId ? edge.StartNodeId : edge.EndNodeId)); + if (endNode is null) continue; + inNodes.Add(endNode); + } + for (int i = 0; i < inNodes.Count - 1; i++) + { + for (int j = i + 1; j < inNodes.Count; j++) + { + if (TStructureExisted(TStructures, node, inNodes[i], inNodes[j])) continue; + var edgeT = Edges.FirstOrDefault(e => (e.StartNodeId == inNodes[i].Id && e.EndNodeId == inNodes[j].Id) || + (e.EndNodeId == inNodes[i].Id && e.StartNodeId == inNodes[j].Id)); + var edge1 = inEdges.FirstOrDefault(edge => edge.StartNodeId == inNodes[i].Id || edge.EndNodeId == inNodes[i].Id); + var edge2 = inEdges.FirstOrDefault(edge => edge.StartNodeId == inNodes[j].Id || edge.EndNodeId == inNodes[j].Id); + if (edgeT is null || edge1 is null || edge2 is null) continue; + if (edgeT.TrajectoryDegree == TrajectoryDegree.One && + edge1.TrajectoryDegree == TrajectoryDegree.One && + edge2.TrajectoryDegree == TrajectoryDegree.One) continue; + + TStructures.Add(new() + { + Node1 = node, + Node2 = inNodes[i], + Node3 = inNodes[j], + Edge12 = edge1, + Edge13 = edge2, + Edge23 = edgeT, + }); + } + } + } + return TStructures; + } + + public static RobotDirection[] GetRobotDirectionInPath(RobotDirection currentDirection, NodeDto[] nodeplanning, EdgeDto[] edgePlanning, double reverseDirectionAngle) + { + RobotDirection[] RobotDirectionInNode = new RobotDirection[nodeplanning.Length]; + if (nodeplanning.Length > 0) RobotDirectionInNode[0] = currentDirection; + if (nodeplanning.Length > 2) + { + for (int i = 1; i < nodeplanning.Length - 1; i++) + { + NodeDto startNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i - 1], edgePlanning[i - 1], Ratio); + NodeDto endNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i + 1], edgePlanning[i], Ratio); ; + var angle = MapEditorHelper.GetAngle(nodeplanning[i], startNode, endNode); + if (angle < reverseDirectionAngle) RobotDirectionInNode[i] = RobotDirectionInNode[i - 1] == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else RobotDirectionInNode[i] = RobotDirectionInNode[i - 1]; + } + } + if (nodeplanning.Length > 1) RobotDirectionInNode[^1] = RobotDirectionInNode[^2]; + return RobotDirectionInNode; + } + + public static RobotDirection GetRobotDirection(NodeDto currentNode, NodeDto futureNode, EdgeDto edge, double robotTheta, double reverseDirectionAngle, bool inGoal) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(currentNode, futureNode, edge, Ratio); + + var RobotDirectionNearNode = new NodeDto() + { + X = currentNode.X + Math.Cos(robotTheta * Math.PI / 180), + Y = currentNode.Y + Math.Sin(robotTheta * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(currentNode, NearNode, RobotDirectionNearNode); + if (angle > reverseDirectionAngle) return inGoal ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + else return inGoal ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + } + + private static double GetNodeAngle(NodeDto node, NodeDto lastNode, EdgeDto edge) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(node, lastNode, edge, Ratio); + return Math.Atan2(node.Y - NearNode.Y, node.X - NearNode.X) * 180 / Math.PI; + } + + private static (bool IsSuccess, NodeDto? intraNode, TStructure? tstructure) IsReverse(NodeDto currentNode, NodeDto olderNode, NodeDto? futureNode, EdgeDto olderedge, EdgeDto? futureedge, double startAngle, List tstructures) + { + var tstructures1 = tstructures.Where(t => t.Node1.Id == currentNode.Id || t.Node2.Id == currentNode.Id || t.Node3.Id == currentNode.Id).ToList(); + if (tstructures1 is null || tstructures1.Count < 1) return (false, null, null); + var tstructures2 = tstructures1.Where(t => t.Node1.Id == olderNode.Id || t.Node2.Id == olderNode.Id || t.Node3.Id == olderNode.Id).ToList(); + if (tstructures2 is null || tstructures2.Count < 1) return (false, null, null); + foreach (var ts in tstructures2) + { + var midleReverse = ts.IsDriectionReverse(currentNode, olderNode); + var intraNode = ts.GetIntraNode(currentNode, olderNode); + if (intraNode is null) continue; + + if (!ts.IsAccessDirection(olderNode, intraNode) || !ts.IsAccessDirection(intraNode, currentNode)) continue; + + var CurrentDirection = GetRobotDirection(olderNode, currentNode, olderedge, startAngle, ReverseDirectionAngleDegree, false); + var intraEdge = ts.GetEdge(olderNode, intraNode); + if (intraEdge is null) continue; + var ReverseDirection = GetRobotDirection(olderNode, intraNode, intraEdge, startAngle, ReverseDirectionAngleDegree, false); + bool firstReverse = ReverseDirection != CurrentDirection; + + bool endReverse = false; + if (futureNode is not null && futureedge is not null) + { + startAngle = GetNodeAngle(currentNode, olderNode, olderedge); + CurrentDirection = GetRobotDirection(currentNode, futureNode, futureedge, startAngle, ReverseDirectionAngleDegree, true); + intraEdge = ts.GetEdge(currentNode, intraNode); + if (intraEdge is null) continue; + startAngle = GetNodeAngle(currentNode, intraNode, intraEdge); + ReverseDirection = GetRobotDirection(currentNode, futureNode, futureedge, startAngle, ReverseDirectionAngleDegree, true); + endReverse = ReverseDirection != CurrentDirection; + } + + if (!midleReverse) + { + if ((!firstReverse && !endReverse) || (firstReverse && endReverse)) continue; + } + else + { + if ((firstReverse && !endReverse) || (!firstReverse && endReverse)) continue; + } + return (true, intraNode, ts); + } + return (false, null, null); + } + + private List GetIntermediateNode(NodeDto startNode, NodeDto endNode) + { + var edge1s = Edges.Where(e => e.StartNodeId == startNode.Id || e.EndNodeId == startNode.Id).ToList(); + var edge2s = Edges.Where(e => e.StartNodeId == endNode.Id || e.EndNodeId == endNode.Id).ToList(); + if (edge1s is null || edge2s is null || edge1s.Count < 2 || edge2s.Count < 2) return []; + List node1 = []; + List IntermediateNode = []; + foreach (var edge1 in edge1s) + { + if (edge1.TrajectoryDegree != TrajectoryDegree.One) continue; + Guid interNodeId = Guid.Empty; + if (edge1.StartNodeId == startNode.Id && (edge1.DirectionAllowed == DirectionAllowed.Both || edge1.DirectionAllowed == DirectionAllowed.Forward)) interNodeId = edge1.EndNodeId; + else if (edge1.DirectionAllowed == DirectionAllowed.Both || edge1.DirectionAllowed == DirectionAllowed.Forward) interNodeId = edge1.StartNodeId; + if (interNodeId == Guid.Empty || interNodeId == endNode.Id) continue; + var interNode = Nodes.FirstOrDefault(n => n.Id == interNodeId); + if (interNode is null) continue; + //(double distance, double x, double y) = PathPlanning.DistanceToRangeSegment(interNode.X, interNode.Y, startNode.X, startNode.Y, endNode.X, endNode.Y); + //if (distance < 0.3 && x != startNode.X && x != endNode.X && y != startNode.Y && y != endNode.Y) + //{ + // node1.Add(interNode); + //} + node1.Add(interNode); + } + if (node1.Count == 0) return []; + foreach (var edge2 in edge2s) + { + if (edge2.TrajectoryDegree != TrajectoryDegree.One) continue; + Guid interNodeId = Guid.Empty; + if (edge2.StartNodeId == endNode.Id && (edge2.DirectionAllowed == DirectionAllowed.Both || edge2.DirectionAllowed == DirectionAllowed.Forward)) interNodeId = edge2.EndNodeId; + else if (edge2.DirectionAllowed == DirectionAllowed.Both || edge2.DirectionAllowed == DirectionAllowed.Forward) interNodeId = edge2.StartNodeId; + if (interNodeId == Guid.Empty || interNodeId == startNode.Id) continue; + var interNode = Nodes.FirstOrDefault(n => n.Id == interNodeId); + if (interNode is null) continue; + //(double distance, double x, double y) = PathPlanning.DistanceToRangeSegment(interNode.X, interNode.Y, startNode.X, startNode.Y, endNode.X, endNode.Y); + //if (distance < 0.3 && x != startNode.X && x != endNode.X && y != startNode.Y && y != endNode.Y) + //{ + // if (node1.Any(n => n.Id == interNode.Id) && !IntermediateNode.Any(n => n.Id == interNode.Id) && interNode.Id != startNode.Id) + // IntermediateNode.Add(interNode); + //} + if (node1.Any(n => n.Id == interNode.Id) && !IntermediateNode.Any(n => n.Id == interNode.Id) && interNode.Id != startNode.Id) + IntermediateNode.Add(interNode); + } + return IntermediateNode; + } + + private EdgeDto[] GetEdgesPlanning(NodeDto[] nodes) + { + var EdgesPlanning = new List(); + for (int i = 0; i < nodes.Length - 1; i++) + { + var edge = Edges.FirstOrDefault(e => e.StartNodeId == nodes[i].Id && e.EndNodeId == nodes[i + 1].Id || + e.EndNodeId == nodes[i].Id && e.StartNodeId == nodes[i + 1].Id); + if (edge is null) + { + if (i != 0) return []; + EdgesPlanning.Add(new EdgeDto() + { + Id = Guid.NewGuid(), + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + }); + continue; + } + bool isReverse = nodes[i].Id != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; ; + EdgesPlanning.Add(new() + { + Id = edge.Id, + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }); + } + return [.. EdgesPlanning]; + } + + private (NodeDto[] NodesFilter, EdgeDto[] EdgesFilter) FilterPathPlanning(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length <= 1 || edges.Length < 1 || nodes.Length - 1 != edges.Length) return ([], []); + List nodeFilter = [nodes[0]]; + for (int i = 1; i < nodes.Length - 1; i++) + { + var IntermediateNode = GetIntermediateNode(nodes[i - 1], nodes[i]); + if (IntermediateNode is null || IntermediateNode.Count == 0) + { + nodeFilter.Add(nodes[i]); + continue; + } + if (IntermediateNode.Any(n => n.Id == nodes[i + 1].Id)) + { + nodeFilter.Add(nodes[i + 1]); + i++; + } + else nodeFilter.Add(nodes[i]); + } + nodeFilter.Add(nodes[^1]); + var edgeFilter = GetEdgesPlanning([.. nodeFilter]); + if (nodeFilter.Count - 1 != edgeFilter.Length) return ([], []); + return ([.. nodeFilter], [.. edgeFilter]); + } + + private double GetLength(EdgeDto[] edges) + { + if (edges.Length == 0) return 999; + double distance = 0; + for (int i = 0; i < edges.Length; i++) + { + var edge = edges.FirstOrDefault(e => e.Id == edges[i].Id); + if (edge is null) return 999; + var startNode = Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) return 999; + distance += MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + X2 = endNode.X, + Y1 = startNode.Y, + Y2 = endNode.Y, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + }); + } + return distance; + } + + private static NodeDto[] CalculatorDirection(NodeDto[] nodes, EdgeDto[] edges, RobotDirection currentDirection) + { + var FinalDirection = GetRobotDirectionInPath(currentDirection, nodes, edges, ReverseDirectionAngleDegree); + NodeDto[] returnNodes = [.. nodes]; + for (var i = 0; i < returnNodes.Length; i++) + { + returnNodes[i].Direction = MapCompute.GetNodeDirection(FinalDirection[i]); + } + return returnNodes; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(x, + y, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y }, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}, {theta}]"); + if (Path.Data.Count == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, RobotDirectionWithAngle, false); + + return new(true) + { + Data = ([.. CalculatorDirection([.. Path.Data], [.. edgeplannings], CurrenDirection)], [.. edgeplannings]) + }; + } + + private MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> CheckPathWithFinalDirection(NodeDto[] nodes, EdgeDto[] edges, double currentAngle, RobotDirection goalDirection = RobotDirection.NONE) + { + if ((nodes[^1].Direction == MapCompute.GetNodeDirection(goalDirection) && GetLength([.. edges]) < 10) || goalDirection == RobotDirection.NONE) + return new(true) { Data = FilterPathPlanning([.. nodes], [.. edges]) }; + + var edgeplannings = edges.ToList(); + var nodeplannings = nodes.ToList(); + + var TStructures = GetTStructure(); + + Guid LastReverseDirectionId = Guid.Empty; + NodeDto LastNodeReverseDirection = new(); + for (int i = 1; i < nodeplannings.Count; i++) + { + if (nodeplannings[i].Direction == Direction.FORWARD) continue; + NodeDto? futureNode = null; + EdgeDto? futureEdge = null; + if (i < nodeplannings.Count - 1) + { + futureNode = nodeplannings[i + 1]; + futureEdge = edgeplannings[i]; + } + double startAngle = currentAngle; + if (i >= 2) startAngle = GetNodeAngle(nodeplannings[i - 1], nodeplannings[i - 2], edgeplannings[i - 2]); + (var IsSuccess, var intraNode, var tstructure) = IsReverse(nodeplannings[i], nodeplannings[i - 1], futureNode, edgeplannings[i - 1], futureEdge, startAngle, TStructures); + if (!IsSuccess || intraNode is null || tstructure is null) continue; + var edge1 = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i - 1].Id && e.EndNodeId == intraNode.Id) || + e.EndNodeId == nodeplannings[i - 1].Id && e.StartNodeId == intraNode.Id); + var edge2 = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i].Id && e.EndNodeId == intraNode.Id) || + e.EndNodeId == nodeplannings[i].Id && e.StartNodeId == intraNode.Id); + if (edge1 is null || edge2 is null) continue; + edgeplannings.RemoveAt(i - 1); + edgeplannings.Insert(i - 1, new() + { + Id = edge1.Id, + StartNodeId = nodeplannings[i - 1].Id, + EndNodeId = intraNode.Id, + TrajectoryDegree = edge1.TrajectoryDegree, + DirectionAllowed = edge1.DirectionAllowed, + ControlPoint1X = edge1.ControlPoint1X, + ControlPoint1Y = edge1.ControlPoint1Y, + ControlPoint2X = edge1.ControlPoint2X, + ControlPoint2Y = edge1.ControlPoint2Y + }); + edgeplannings.Insert(i, new() + { + Id = edge2.Id, + StartNodeId = intraNode.Id, + EndNodeId = nodeplannings[i].Id, + TrajectoryDegree = edge2.TrajectoryDegree, + DirectionAllowed = edge2.DirectionAllowed, + ControlPoint1X = edge2.ControlPoint1X, + ControlPoint1Y = edge2.ControlPoint1Y, + ControlPoint2X = edge2.ControlPoint2X, + ControlPoint2Y = edge2.ControlPoint2Y + }); + nodeplannings.Insert(i, intraNode); + var directionInPath = GetRobotDirectionInPath(MapCompute.GetRobotDirection(nodeplannings[0].Direction), [.. nodeplannings], [.. edgeplannings], ReverseDirectionAngleDegree); + foreach (var node in nodeplannings) + { + node.Direction = MapCompute.GetNodeDirection(directionInPath[nodeplannings.IndexOf(node)]); + } + LastReverseDirectionId = tstructure.Id; + LastNodeReverseDirection = nodeplannings[i + 1]; + i++; + } + + if (nodeplannings[^1].Direction == MapCompute.GetNodeDirection(goalDirection)) + return new(true) { Data = FilterPathPlanning([.. nodeplannings], [.. edgeplannings]) }; + + for (int i = nodeplannings.Count - 1; i > 0; i--) + { + NodeDto? futureNode = null; + EdgeDto? futureEdge = null; + if (i < nodeplannings.Count - 1) + { + futureNode = nodeplannings[i + 1]; + futureEdge = edgeplannings[i]; + } + double startAngle = currentAngle; + if (i >= 2) startAngle = GetNodeAngle(nodeplannings[i - 1], nodeplannings[i - 2], edgeplannings[i - 2]); + (var IsSuccess, var intraNode, var tstructure) = IsReverse(nodeplannings[i], nodeplannings[i - 1], futureNode, edgeplannings[i - 1], futureEdge, startAngle, TStructures); + if (!IsSuccess || intraNode is null || tstructure is null) continue; + + if (nodeplannings[i - 1].Id == LastNodeReverseDirection.Id) + { + var edge = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i - 2].Id && e.EndNodeId == nodeplannings[i].Id) || + (e.StartNodeId == nodeplannings[i].Id && e.EndNodeId == nodeplannings[i - 2].Id)); + if (edge is null) continue; + edgeplannings.Insert(i - 2, new() + { + Id = edge.Id, + StartNodeId = nodeplannings[i - 2].Id, + EndNodeId = nodeplannings[i].Id, + TrajectoryDegree = edge.TrajectoryDegree, + DirectionAllowed = edge.DirectionAllowed, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y + }); + edgeplannings.RemoveAt(i); + edgeplannings.RemoveAt(i - 1); + nodeplannings.RemoveAt(i - 1); + } + else if (tstructure.Id != LastReverseDirectionId || i < 2) + { + var edge1 = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i - 1].Id && e.EndNodeId == intraNode.Id) || + e.EndNodeId == nodeplannings[i - 1].Id && e.StartNodeId == intraNode.Id); + var edge2 = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i].Id && e.EndNodeId == intraNode.Id) || + e.EndNodeId == nodeplannings[i].Id && e.StartNodeId == intraNode.Id); + if (edge1 is null || edge2 is null) continue; + edgeplannings.RemoveAt(i - 1); + edgeplannings.Insert(i - 1, new() + { + Id = edge1.Id, + StartNodeId = nodeplannings[i - 1].Id, + EndNodeId = intraNode.Id, + TrajectoryDegree = edge1.TrajectoryDegree, + DirectionAllowed = edge1.DirectionAllowed, + ControlPoint1X = edge1.ControlPoint1X, + ControlPoint1Y = edge1.ControlPoint1Y, + ControlPoint2X = edge1.ControlPoint2X, + ControlPoint2Y = edge1.ControlPoint2Y, + }); + edgeplannings.Insert(i, new() + { + Id = edge2.Id, + StartNodeId = intraNode.Id, + EndNodeId = nodeplannings[i].Id, + TrajectoryDegree = edge2.TrajectoryDegree, + DirectionAllowed = edge2.DirectionAllowed, + ControlPoint1X = edge2.ControlPoint1X, + ControlPoint1Y = edge2.ControlPoint1Y, + ControlPoint2X = edge2.ControlPoint2X, + ControlPoint2Y = edge2.ControlPoint2Y, + }); + nodeplannings.Insert(i, intraNode); + } + else + { + var edge = Edges.FirstOrDefault(e => (e.StartNodeId == nodeplannings[i - 2].Id && e.EndNodeId == nodeplannings[i].Id) || + (e.StartNodeId == nodeplannings[i].Id && e.EndNodeId == nodeplannings[i - 2].Id)); + if (edge is null) continue; + edgeplannings.Insert(i - 2, new() + { + Id = edge.Id, + StartNodeId = nodeplannings[i - 2].Id, + EndNodeId = nodeplannings[i].Id, + TrajectoryDegree = edge.TrajectoryDegree, + DirectionAllowed = edge.DirectionAllowed, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + }); + edgeplannings.RemoveAt(i); + edgeplannings.RemoveAt(i - 1); + nodeplannings.RemoveAt(i - 1); + } + return new(true) { Data = FilterPathPlanning([.. CalculatorDirection([.. nodeplannings], [.. edgeplannings], MapCompute.GetRobotDirection(nodeplannings[0].Direction))], [.. edgeplannings]) }; + } + return new(false, $"Đường đi đến đích không thỏa mãn điều kiện"); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, RobotDirectionWithAngle, true); + + return CheckPathWithFinalDirection(basicPath.Data.Nodes, basicPath.Data.Edges, theta, goalDirection); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(double x, double y, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(double x, double y, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + return CheckPathWithFinalDirection(basicPath.Data.Nodes, basicPath.Data.Edges, theta, goalDirection); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(startNode, goal, cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Length < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + if (Path.Data.Length == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, RobotDirectionWithAngle, false); + + return new(true) + { + Data = ([.. CalculatorDirection([.. Path.Data], [.. edgeplannings], CurrenDirection)], [.. edgeplannings]) + }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, RobotDirectionWithAngle, true); + + return CheckPathWithFinalDirection(basicPath.Data.Nodes, basicPath.Data.Edges, theta, goalDirection); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + return CheckPathWithFinalDirection(basicPath.Data.Nodes, basicPath.Data.Edges, theta, goalDirection); + } +} diff --git a/RobotNet.RobotManager/Services/Planner/Fokrlift/TStructure.cs b/RobotNet.RobotManager/Services/Planner/Fokrlift/TStructure.cs new file mode 100644 index 0000000..44190f1 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Fokrlift/TStructure.cs @@ -0,0 +1,135 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.RobotManager.Services.Planner.Fokrlift; +public enum TStructureDirection +{ + NODE1_NODE2_NODE3, + NODE1_NODE3_NODE2, + NODE2_NODE1_NODE3, + NODE2_NODE3_NODE1, + NODE3_NODE2_NODE1, + NODE3_NODE1_NODE2, +} + +public class TStructure +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public NodeDto Node1 { get; set; } = new(); + public NodeDto Node2 { get; set; } = new(); + public NodeDto Node3 { get; set; } = new(); + public EdgeDto Edge12 { get; set; } = new(); + public EdgeDto Edge13 { get; set; } = new(); + public EdgeDto Edge23 { get; set; } = new(); + private const double Ratio = 0.1; + + public bool IsDriectionReverse(TStructureDirection direction) + { + NodeDto OriginNode = new(); + NodeDto ToWardNode1 = new(); + NodeDto ToWardNode2 = new(); + EdgeDto ToWardEdge1 = new(); + EdgeDto ToWardEdge2 = new(); + + switch (direction) + { + case TStructureDirection.NODE3_NODE2_NODE1: + case TStructureDirection.NODE1_NODE2_NODE3: + OriginNode = Node2; + ToWardNode1 = Node1; + ToWardNode2 = Node3; + ToWardEdge1 = Edge12; + ToWardEdge2 = Edge23; + break; + case TStructureDirection.NODE2_NODE3_NODE1: + case TStructureDirection.NODE1_NODE3_NODE2: + OriginNode = Node3; + ToWardNode1 = Node1; + ToWardNode2 = Node2; + ToWardEdge1 = Edge13; + ToWardEdge2 = Edge23; + break; + case TStructureDirection.NODE3_NODE1_NODE2: + case TStructureDirection.NODE2_NODE1_NODE3: + OriginNode = Node1; + ToWardNode1 = Node2; + ToWardNode2 = Node3; + ToWardEdge1 = Edge12; + ToWardEdge2 = Edge13; + break; + } + + var NearToWardNode1 = MapEditorHelper.GetNearByNode(OriginNode, ToWardNode1, ToWardEdge1, Ratio); + var NearToWardNode3 = MapEditorHelper.GetNearByNode(OriginNode, ToWardNode2, ToWardEdge2, Ratio); + var angle = MapEditorHelper.GetAngle(OriginNode, NearToWardNode1, NearToWardNode3); + if (angle < 50) return true; + return false; + } + + public bool IsDriectionReverse(NodeDto node1, NodeDto node2) + { + if (node1.Id == Node1.Id) + { + if (node2.Id == Node2.Id) return IsDriectionReverse(TStructureDirection.NODE1_NODE3_NODE2); + else if (node2.Id == Node3.Id) return IsDriectionReverse(TStructureDirection.NODE1_NODE2_NODE3); + } + else if (node1.Id == Node2.Id) + { + if (node2.Id == Node1.Id) return IsDriectionReverse(TStructureDirection.NODE2_NODE3_NODE1); + else if (node2.Id == Node3.Id) return IsDriectionReverse(TStructureDirection.NODE2_NODE1_NODE3); + } + else if (node1.Id == Node3.Id) + { + if (node2.Id == Node1.Id) return IsDriectionReverse(TStructureDirection.NODE3_NODE2_NODE1); + else if (node2.Id == Node2.Id) return IsDriectionReverse(TStructureDirection.NODE3_NODE1_NODE2); + } + return false; + } + + public NodeDto? GetIntraNode(NodeDto node1, NodeDto node2) + { + if (node1.Id == Node1.Id) + { + if (node2.Id == Node2.Id) return Node3; + else if (node2.Id == Node3.Id) return Node2; + } + else if (node1.Id == Node2.Id) + { + if (node2.Id == Node1.Id) return Node3; + else if (node2.Id == Node3.Id) return Node1; + } + else if (node1.Id == Node3.Id) + { + if (node2.Id == Node1.Id) return Node2; + else if (node2.Id == Node2.Id) return Node1; + } + return null; + } + + public EdgeDto? GetEdge(NodeDto node1, NodeDto node2) + { + if (Edge12.StartNodeId == node1.Id || Edge12.EndNodeId == node1.Id) + { + if (Edge12.StartNodeId == node2.Id || Edge12.EndNodeId == node2.Id) return Edge12; + } + if (Edge13.StartNodeId == node1.Id || Edge13.EndNodeId == node1.Id) + { + if (Edge13.StartNodeId == node2.Id || Edge13.EndNodeId == node2.Id) return Edge13; + } + if (Edge23.StartNodeId == node1.Id || Edge23.EndNodeId == node1.Id) + { + if (Edge23.StartNodeId == node2.Id || Edge23.EndNodeId == node2.Id) return Edge23; + } + return null; + } + + public bool IsAccessDirection(NodeDto startNode, NodeDto endNode) + { + var edge = GetEdge(startNode, endNode); + if (edge is null) return false; + if (edge.StartNodeId == startNode.Id && (edge.DirectionAllowed == DirectionAllowed.Both || edge.DirectionAllowed == DirectionAllowed.Forward)) return true; + if (edge.EndNodeId == startNode.Id && (edge.DirectionAllowed == DirectionAllowed.Both || edge.DirectionAllowed == DirectionAllowed.Backward)) return true; + return false; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/ForkliftV2/ForkLiftPathPlannerV2.cs b/RobotNet.RobotManager/Services/Planner/ForkliftV2/ForkLiftPathPlannerV2.cs new file mode 100644 index 0000000..34491a0 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/ForkliftV2/ForkLiftPathPlannerV2.cs @@ -0,0 +1,310 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.AStar; +using RobotNet.RobotManager.Services.Planner.Fokrlift; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.RobotShares.Models; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.ForkliftV2; + +public class ForkLiftPathPlannerV2 : IPathPlanner +{ + private List Nodes = []; + private List Edges = []; + private const double ReverseDirectionAngleDegree = 80; + private const double RobotDirectionWithAngle = 90; + private const double Ratio = 0.1; + + private readonly PathPlanningOptions Options = new() + { + LimitDistanceToEdge = 1, + LimitDistanceToNode = 0.3, + ResolutionSplit = 0.1, + }; + + public void SetData(NodeDto[] nodes, EdgeDto[] edges) + { + Nodes = [.. nodes]; + Edges = [.. edges]; + } + + public void SetOptions(PathPlanningOptions options) + { + Options.LimitDistanceToNode = options.LimitDistanceToNode; + Options.LimitDistanceToEdge = options.LimitDistanceToEdge; + Options.ResolutionSplit = options.ResolutionSplit; + } + + public static RobotDirection[] GetRobotDirectionInPath(RobotDirection currentDirection, List nodeplanning, List edgePlanning) + { + RobotDirection[] RobotDirectionInNode = new RobotDirection[nodeplanning.Count]; + if (nodeplanning.Count > 0) RobotDirectionInNode[0] = currentDirection; + if (nodeplanning.Count > 2) + { + for (int i = 1; i < nodeplanning.Count - 1; i++) + { + NodeDto startNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i - 1], edgePlanning[i - 1], Ratio); + NodeDto endNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i + 1], edgePlanning[i], Ratio); ; + var angle = MapEditorHelper.GetAngle(nodeplanning[i], startNode, endNode); + if (angle < ReverseDirectionAngleDegree) RobotDirectionInNode[i] = RobotDirectionInNode[i - 1] == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else RobotDirectionInNode[i] = RobotDirectionInNode[i - 1]; + } + } + if (nodeplanning.Count > 1) RobotDirectionInNode[^1] = RobotDirectionInNode[^2]; + return RobotDirectionInNode; + } + + public static RobotDirection GetRobotDirection(NodeDto currentNode, NodeDto nearNode, EdgeDto edge, double robotInNodeAngle, bool isReverse) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(currentNode, nearNode, edge, Ratio); + + var RobotNearNode = new NodeDto() + { + X = currentNode.X + Math.Cos(robotInNodeAngle * Math.PI / 180), + Y = currentNode.Y + Math.Sin(robotInNodeAngle * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(currentNode, NearNode, RobotNearNode); + if (angle > ReverseDirectionAngleDegree) return isReverse ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else return isReverse ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + } + + private static NodeDto[] CalculatorDirection(List nodes, List edges, RobotDirection currentDirection) + { + var FinalDirection = GetRobotDirectionInPath(currentDirection, nodes, edges); + NodeDto[] returnNodes = [.. nodes]; + foreach (var item in returnNodes) + { + item.Direction = MapCompute.GetNodeDirection(FinalDirection[nodes.IndexOf(item)]); + } + return returnNodes; + } + + private List GetEdgesPlanning(List nodes) + { + var EdgesPlanning = new List(); + for (int i = 0; i < nodes.Count - 1; i++) + { + var edge = Edges.FirstOrDefault(e => e.StartNodeId == nodes[i].Id && e.EndNodeId == nodes[i + 1].Id || + e.EndNodeId == nodes[i].Id && e.StartNodeId == nodes[i + 1].Id); + if (edge is null) + { + if (i != 0) return []; + EdgesPlanning.Add(new EdgeDto() + { + Id = Guid.NewGuid(), + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + }); + continue; + } + bool isReverse = nodes[i].Id != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; ; + EdgesPlanning.Add(new() + { + Id = edge.Id, + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }); + } + return EdgesPlanning; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(x, + y, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y }, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}, {theta}]") { Data = ([goal], []) }; + if (Path.Data.Count == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + return new(true) { Data = ([.. CalculatorDirection(Path.Data, edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var SSEAStarPathPlanner = new SSEAStarPathPlanner(Nodes, Edges); + var Path = SSEAStarPathPlanner.PlanningWithGoalAngle(x, + y, + theta, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y, Theta = goal.Theta }, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess|| Path.Data is null || Path.Data.Count < 1) + { + var ForkliftV1 = new ForkliftPathPlanner(); + ForkliftV1.SetData([.. Nodes], [.. Edges]); + return ForkliftV1.PathPlanningWithAngle(x, + y, + theta, + goal.Id, + cancellationToken); + } + else + { + if (Path.Data.Count == 1) return new(true, $"Robot đang đứng tại điểm đích [{goal.X}, {goal.Y}], robot: [{x}, {y}, {theta}]") { Data = ([goal], [])}; + + var edgeplannings = GetEdgesPlanning(Path.Data); + if (edgeplannings.Count < 1) return new(false, $"Không thể lấy ra các đoạn thuộc tuyến đường đã tìm ra"); + + return new(true) { Data = ([.. CalculatorDirection(Path.Data, edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(double x, double y, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(double x, double y, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var SSEAStarPathPlanner = new SSEAStarPathPlanner(Nodes, Edges); + var Path = SSEAStarPathPlanner.PlanningWithFinalDirection(x, + y, + theta, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y }, + goalDirection, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess || Path.Data is null || Path.Data.Count < 1) + { + var ForkliftV1 = new ForkliftPathPlanner(); + ForkliftV1.SetData([.. Nodes], [.. Edges]); + return ForkliftV1.PathPlanningWithFinalDirection(x, + y, + theta, + goal.Id, + goalDirection, + cancellationToken); + } + else + { + if (Path.Data.Count == 1) return new(true, $"Robot đang đứng tại điểm đích [{goal.X}, {goal.Y}], robot: [{x}, {y}, {theta}]") { Data = ([goal], []) }; + + var edgeplannings = GetEdgesPlanning(Path.Data); + if (edgeplannings.Count < 1) return new(false, $"Không thể lấy ra các đoạn thuộc tuyến đường đã tìm ra"); + + return new(true) { Data = ([.. CalculatorDirection(Path.Data, edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(startNode, + goal, + cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Length < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]") { Data = ([goal], []) }; + if (Path.Data.Length == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + return new(true) { Data = ([.. CalculatorDirection([..Path.Data], edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var SSEAStarPathPlanner = new SSEAStarPathPlanner(Nodes, Edges); + var Path = SSEAStarPathPlanner.PlanningWithGoalAngle(startNode, + theta, + goal, + cancellationToken); + if (!Path.IsSuccess || Path.Data is null || Path.Data.Length < 1) + { + var ForkliftV1 = new ForkliftPathPlanner(); + ForkliftV1.SetData([.. Nodes], [.. Edges]); + return ForkliftV1.PathPlanningWithAngle(startNodeId, + theta, + goal.Id, + cancellationToken); + } + else + { + if (Path.Data.Length == 1) return new(true, $"Robot đang đứng tại điểm đích [{goal.X}, {goal.Y}], robot: [{startNode.Name} - {startNode.Id}]") { Data = ([goal], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + if (edgeplannings.Count < 1) return new(false, $"Không thể lấy ra các đoạn thuộc tuyến đường đã tìm ra"); + + return new(true) { Data = ([.. CalculatorDirection([.. Path.Data], edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + throw new NotImplementedException(); + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var SSEAStarPathPlanner = new SSEAStarPathPlanner(Nodes, Edges); + var Path = SSEAStarPathPlanner.PlanningWithFinalDirection(startNode, + theta, + goal, + goalDirection, + cancellationToken); + if (!Path.IsSuccess || Path.Data is null || Path.Data.Length < 1) + { + var ForkliftV1 = new ForkliftPathPlanner(); + ForkliftV1.SetData([.. Nodes], [.. Edges]); + return ForkliftV1.PathPlanningWithFinalDirection(startNodeId, + theta, + goal.Id, + goalDirection, + cancellationToken); + } + else + { + if (Path.Data.Length == 1) return new(true, $"Robot đang đứng tại điểm đích [{goal.X}, {goal.Y}], robot: [{startNode.Name} - {startNode.Id}]") { Data = ([goal], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + if (edgeplannings.Count < 1) return new(false, $"Không thể lấy ra các đoạn thuộc tuyến đường đã tìm ra"); + + return new(true) { Data = ([.. CalculatorDirection([.. Path.Data], edgeplannings, MapCompute.GetRobotDirection(Path.Data[0].Direction))], [.. edgeplannings]), }; + } + } +} diff --git a/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarNode.cs b/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarNode.cs new file mode 100644 index 0000000..b015088 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarNode.cs @@ -0,0 +1,31 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Planner.ForkliftV2; + +#nullable disable + +public class SSEAStarNode +{ + public Guid Id { get; set; } + public double X { get; set; } + public double Y { get; set; } + public RobotDirection Direction { get; set; } + public double Cost { get; set; } + public double Heuristic { get; set; } + public double TotalCost => Cost + Heuristic; + public string Name { get; set; } + public SSEAStarNode Parent { get; set; } + public List NegativeNodes { get; set; } = []; + + public override bool Equals(object obj) + { + if (obj is SSEAStarNode other) + return Id == other.Id && Direction == other.Direction; + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Direction); + } +} diff --git a/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarPathPlanner.cs b/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarPathPlanner.cs new file mode 100644 index 0000000..dce64f4 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/ForkliftV2/SSEAStarPathPlanner.cs @@ -0,0 +1,621 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.ForkliftV2; + +public class SSEAStarPathPlanner(List Nodes, List Edges) +{ + private NodeDto? GetOnNode(double x, double y, double limitDistance, CancellationToken? cancellationToken = null) + { + if (cancellationToken?.IsCancellationRequested == true) return null; + KDTree KDTree = new(Nodes); + return KDTree.FindNearest(x, y, limitDistance); + } + + //public EdgeDto? GetClosesEdge(double x, double y, double limitDistance, CancellationToken? cancellationToken = null) + //{ + // if (cancellationToken?.IsCancellationRequested == true) return null; + // RTree RTree = new(Nodes, Edges); + // return RTree.FindNearest(x, y, limitDistance); + //} + + public EdgeDto? GetClosesEdge(double x, double y, double limitDistance, CancellationToken? cancellationToken = null) + { + double minDistance = double.MaxValue; + EdgeDto? edgeResult = null; + foreach (var edge in Edges) + { + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return null; + var startNode = Nodes.FirstOrDefault(node => node.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(node => node.Id == edge.EndNodeId); + if (startNode is null || endNode is null) continue; + + var getDistance = MapCompute.DistanceToEdge(x, y, edge, startNode, endNode); + if (getDistance.IsSuccess) + { + if (getDistance.Data < minDistance) + { + minDistance = getDistance.Data; + edgeResult = edge; + } + } + } + if (minDistance <= limitDistance) return edgeResult; + else return null; + } + + public List GetNegativeNode(Guid nodeId, CancellationToken? cancellationToken = null) + { + var node = Nodes.FirstOrDefault(p => p.Id == nodeId); + if (node is null) return []; + + var listNodesNegative = new List(); + var listPaths = Edges.Where(p => p.EndNodeId == nodeId || p.StartNodeId == nodeId); + foreach (var path in listPaths) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + if (path.StartNodeId == node.Id && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Forward)) + { + var nodeAdd = Nodes.FirstOrDefault(p => p.Id == path.EndNodeId); + if (nodeAdd != null) listNodesNegative.Add(nodeAdd); + continue; + } + if (path.EndNodeId == node.Id && (path.DirectionAllowed == DirectionAllowed.Both || path.DirectionAllowed == DirectionAllowed.Backward)) + { + var nodeAdd = Nodes.FirstOrDefault(p => p.Id == path.StartNodeId); + if (nodeAdd != null) listNodesNegative.Add(nodeAdd); + continue; + } + } + return listNodesNegative; + } + + private double GetNegativeCost(SSEAStarNode currentNode, SSEAStarNode negativeNode) + { + var negativeEdges = Edges.Where(e => e.StartNodeId == currentNode.Id && e.EndNodeId == negativeNode.Id || + e.StartNodeId == negativeNode.Id && e.EndNodeId == currentNode.Id).ToList(); + double minDistance = double.MaxValue; + foreach (var edge in negativeEdges) + { + var startNode = Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode is null || endNode is null) return 0; + var distance = MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }); + if (distance < minDistance) minDistance = distance; + } + return minDistance != double.MaxValue ? minDistance : 0; + } + + private List GetNegativeAStarNode(SSEAStarNode nodeCurrent, NodeDto endNode, CancellationToken? cancellationToken = null) + { + var possiblePointNegative = new List(); + if (nodeCurrent.Id == endNode.Id) return possiblePointNegative; + + var listNodesNegative = GetNegativeNode(nodeCurrent.Id, cancellationToken); + + foreach (var item in listNodesNegative) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + if (nodeCurrent.Parent is null) continue; + var nodeDtoCurrent = Nodes.FirstOrDefault(n => n.Id == nodeCurrent.Id); + var nodeDtoNegative = Nodes.FirstOrDefault(n => n.Id == item.Id); + var nodeDtoParent = Nodes.FirstOrDefault(n => n.Id == nodeCurrent.Parent.Id); + var negativeEdge = Edges.FirstOrDefault(e => e.StartNodeId == nodeCurrent.Id && e.EndNodeId == item.Id || e.EndNodeId == nodeCurrent.Id && e.StartNodeId == item.Id); + var parentEdge = Edges.FirstOrDefault(e => e.StartNodeId == nodeCurrent.Id && e.EndNodeId == nodeCurrent.Parent.Id || e.EndNodeId == nodeCurrent.Id && e.StartNodeId == nodeCurrent.Parent.Id); + + if (nodeDtoCurrent is null || nodeDtoNegative is null || negativeEdge is null) continue; + + var nearNodeNevgative = MapEditorHelper.GetNearByNode(nodeDtoCurrent, nodeDtoNegative, negativeEdge, 0.01); + var nearNodeParent = nodeDtoParent is not null && parentEdge is not null ? MapEditorHelper.GetNearByNode(nodeDtoCurrent, nodeDtoParent, parentEdge, 0.01) : + new() + { + Id = nodeCurrent.Parent.Id, + X = nodeCurrent.Parent.X, + Y = nodeCurrent.Parent.Y, + Name = nodeCurrent.Parent.Name + }; + + var angle = MapEditorHelper.GetAngle(nodeDtoCurrent, nearNodeNevgative, nearNodeParent); + RobotDirection direction = angle < 89 ? nodeCurrent.Direction == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD : nodeCurrent.Direction; + + var nodeNegative = new SSEAStarNode + { + Id = item.Id, + X = item.X, + Y = item.Y, + Name = item.Name, + Direction = direction, + Parent = nodeCurrent + }; + + var cost = GetNegativeCost(nodeCurrent, nodeNegative); + cost = cost > 0 ? cost : Math.Sqrt(Math.Pow(nodeCurrent.X - nodeNegative.X, 2) + Math.Pow(nodeCurrent.Y - nodeNegative.Y, 2)); + nodeNegative.Cost = cost + nodeCurrent.Cost + (direction == RobotDirection.BACKWARD ? cost * Math.Sqrt(2) / 2 : 0.0); + var distance = Math.Abs(endNode.X - nodeNegative.X) + Math.Abs(endNode.Y - nodeNegative.Y); + nodeNegative.Heuristic = distance + (direction == RobotDirection.BACKWARD ? distance : 0.0); + possiblePointNegative.Add(nodeNegative); + } + if (nodeCurrent.NegativeNodes is not null && nodeCurrent.NegativeNodes.Count > 0) possiblePointNegative.AddRange(nodeCurrent.NegativeNodes); + return possiblePointNegative; + } + + public List Find(SSEAStarNode startNode, NodeDto endNode, RobotDirection goalDirection, CancellationToken? cancellationToken = null) + { + try + { + var activeNodes = new PriorityQueue((a, b) => a.TotalCost.CompareTo(b.TotalCost)); + var visitedNodes = new HashSet(); + var path = new List(); + var shortestPath = new HashSet(); + + activeNodes.Enqueue(startNode); + + while (activeNodes.Count > 0 && (!cancellationToken.HasValue || !cancellationToken.Value.IsCancellationRequested)) + { + var checkNode = activeNodes.Dequeue(); + if (checkNode.Id == endNode.Id) + { + if (checkNode.Direction == goalDirection) + { + var node = checkNode; + while (node != null) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + path.Add(node); + node = node.Parent; + } + return path; + } + else + { + var node = checkNode; + while (node != null) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + shortestPath.Add(node); + node = node.Parent; + } + } + } + + visitedNodes.Add(checkNode); + + var listNodeNegative = GetNegativeAStarNode(checkNode, endNode, cancellationToken); + foreach (var node in listNodeNegative) + { + if (visitedNodes.TryGetValue(node, out SSEAStarNode? value) && value is not null) + { + if (value.TotalCost > node.TotalCost || shortestPath.Any(n => n.Id == node.Id) && value.Parent is not null && value.Parent.Heuristic < checkNode.Heuristic) + { + visitedNodes.Remove(value); + activeNodes.Enqueue(node); + } + continue; + } + + var activeNode = activeNodes.Items.FirstOrDefault(n => n.Id == node.Id && n.Direction == node.Direction); + if (activeNode is not null && activeNode.TotalCost > node.TotalCost) + { + activeNodes.Items.Remove(activeNode); + activeNodes.Enqueue(node); + } + else + { + activeNodes.Enqueue(node); + } + } + } + + return []; + } + catch + { + return []; + } + } + + public List Find(SSEAStarNode startNode, NodeDto endNode, CancellationToken? cancellationToken = null) + { + try + { + var activeNodes = new PriorityQueue((a, b) => a.TotalCost.CompareTo(b.TotalCost)); + var visitedNodes = new HashSet(); + var path = new List(); + var shortestPath = new HashSet(); + + activeNodes.Enqueue(startNode); + + while (activeNodes.Count > 0 && (!cancellationToken.HasValue || !cancellationToken.Value.IsCancellationRequested)) + { + var checkNode = activeNodes.Dequeue(); + if (checkNode.Id == endNode.Id) + { + if (checkNode.Parent is not null) + { + var nodeParentDto = Nodes.FirstOrDefault(n => n.Id == checkNode.Parent.Id); + var edge = Edges.FirstOrDefault(e => e.StartNodeId == checkNode.Id && e.EndNodeId == checkNode.Parent.Id || e.EndNodeId == checkNode.Id && e.StartNodeId == checkNode.Parent.Id); + if (edge is not null && nodeParentDto is not null) + { + var nearParent = MapEditorHelper.GetNearByNode(endNode, nodeParentDto, edge, 0.01); + var nearGoalNode = new NodeDto() + { + X = endNode.X + Math.Cos(endNode.Theta * Math.PI / 180), + Y = endNode.Y + Math.Sin(endNode.Theta * Math.PI / 180), + }; + + var angle = MapEditorHelper.GetAngle(endNode, nearParent, nearGoalNode); + RobotDirection goalDirection = angle < 89 ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + if (checkNode.Direction == goalDirection) + { + var returnNode = checkNode; + while (returnNode != null) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + path.Add(returnNode); + returnNode = returnNode.Parent; + } + return path; + } + } + } + + var node = checkNode; + while (node != null) + { + if (cancellationToken.HasValue && cancellationToken.Value.IsCancellationRequested) return []; + shortestPath.Add(node); + node = node.Parent; + } + } + + visitedNodes.Add(checkNode); + + var listNodeNegative = GetNegativeAStarNode(checkNode, endNode, cancellationToken); + foreach (var node in listNodeNegative) + { + if (visitedNodes.TryGetValue(node, out SSEAStarNode? value) && value is not null) + { + if (value.TotalCost > node.TotalCost || shortestPath.Any(n => n.Id == node.Id) && value.Parent is not null && value.Parent.Heuristic < checkNode.Heuristic) + { + visitedNodes.Remove(value); + activeNodes.Enqueue(node); + } + continue; + } + + var activeNode = activeNodes.Items.FirstOrDefault(n => n.Id == node.Id && n.Direction == node.Direction); + if (activeNode is not null && activeNode.TotalCost > node.TotalCost) + { + activeNodes.Items.Remove(activeNode); + activeNodes.Enqueue(node); + } + else + { + activeNodes.Enqueue(node); + } + } + } + + return []; + } + catch + { + return []; + } + } + + private SSEAStarNode GetClosesNode(NodeDto closesNode, NodeDto goal, double theta, CancellationToken? cancellationToken = null) + { + SSEAStarNode closesAStarNode = new() + { + Id = closesNode.Id, + X = closesNode.X, + Y = closesNode.Y, + Name = closesNode.Name, + }; + foreach (var negativeNode in GetNegativeNode(closesAStarNode.Id, cancellationToken)) + { + SSEAStarNode closesAStarNodeParent = new() + { + Id = closesNode.Id, + X = closesNode.X, + Y = closesNode.Y, + Name = closesNode.Name, + }; + var RobotNearNode = new NodeDto() + { + X = closesAStarNode.X + Math.Cos(theta * Math.PI / 180), + Y = closesAStarNode.Y + Math.Sin(theta * Math.PI / 180), + }; + + var angle = MapEditorHelper.GetAngle(closesNode, negativeNode, RobotNearNode); + RobotDirection direction = angle < 91 ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + + var cost = GetNegativeCost(closesAStarNode, new() { Id = negativeNode.Id, X = negativeNode.X, Y = negativeNode.Y }); + cost = cost > 0 ? cost : Math.Sqrt(Math.Pow(closesAStarNode.X - negativeNode.X, 2) + Math.Pow(closesAStarNode.Y - negativeNode.Y, 2)); + cost += direction == RobotDirection.BACKWARD ? cost * Math.Sqrt(2) / 2 : 0.0; + closesAStarNodeParent.Direction = direction; + closesAStarNode.NegativeNodes.Add(new() + { + Id = negativeNode.Id, + X = negativeNode.X, + Y = negativeNode.Y, + Name = negativeNode.Name, + Direction = direction, + Cost = cost, + Heuristic = Math.Abs(goal.X - negativeNode.X) + Math.Abs(goal.Y - negativeNode.Y), + Parent = closesAStarNodeParent, + }); + } + return closesAStarNode; + } + + private static SSEAStarNode[] GetClosesEdge(EdgeDto closesEdge, NodeDto goal, NodeDto startNodeForward, NodeDto startNodeBackward, SSEAStarNode robotNode, double theta) + { + List negativeNodes = []; + if (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Backward) + { + SSEAStarNode closesAStarNodeParent = new() + { + Id = robotNode.Id, + X = robotNode.X, + Y = robotNode.Y, + Name = robotNode.Name, + }; + var RobotNearNode = new NodeDto() + { + X = robotNode.X + Math.Cos(theta * Math.PI / 180), + Y = robotNode.Y + Math.Sin(theta * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(new() { X = robotNode.X, Y = robotNode.Y, Theta = theta }, startNodeForward, RobotNearNode); + RobotDirection direction = angle < 91 ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + + double cost = Math.Sqrt(Math.Pow(robotNode.X - startNodeBackward.X, 2) + Math.Pow(robotNode.Y - startNodeBackward.Y, 2)); + cost += direction == RobotDirection.BACKWARD ? cost * Math.Sqrt(2) / 2 : 0.0; + closesAStarNodeParent.Direction = direction; + negativeNodes.Add(new() + { + Id = startNodeForward.Id, + X = startNodeForward.X, + Y = startNodeForward.Y, + Name = startNodeForward.Name, + Direction = direction, + Cost = cost, + Heuristic = Math.Abs(goal.X - startNodeForward.X) + Math.Abs(goal.Y - startNodeForward.Y), + Parent = closesAStarNodeParent, + }); + } + if (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Forward) + { + SSEAStarNode closesAStarNodeParent = new() + { + Id = robotNode.Id, + X = robotNode.X, + Y = robotNode.Y, + Name = robotNode.Name, + }; + var RobotNearNode = new NodeDto() + { + X = robotNode.X + Math.Cos(theta * Math.PI / 180), + Y = robotNode.Y + Math.Sin(theta * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(new() { X = robotNode.X, Y = robotNode.Y, Theta = theta }, startNodeBackward, RobotNearNode); + RobotDirection direction = angle < 91 ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + + double cost = Math.Sqrt(Math.Pow(robotNode.X - startNodeBackward.X, 2) + Math.Pow(robotNode.Y - startNodeBackward.Y, 2)); + cost += direction == RobotDirection.BACKWARD ? cost * Math.Sqrt(2) / 2 : 0.0; + closesAStarNodeParent.Direction = direction; + negativeNodes.Add(new() + { + Id = startNodeBackward.Id, + X = startNodeBackward.X, + Y = startNodeBackward.Y, + Name = startNodeBackward.Name, + Direction = direction, + Cost = cost, + Heuristic = Math.Abs(goal.X - startNodeBackward.X) + Math.Abs(goal.Y - startNodeBackward.Y), + Parent = closesAStarNodeParent, + }); + } + return [.. negativeNodes]; + } + + public MessageResult> PlanningWithFinalDirection(double x, double y, double theta, NodeDto goal, RobotDirection goalDirection, double maxDistanceToEdge, double maxDistanceToNode, CancellationToken? cancellationToken = null) + { + var Path = new List(); + + SSEAStarNode RobotNode = new() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + Name = "RobotCurrentNode", + }; + var closesNode = GetOnNode(x, y, maxDistanceToNode); + if (closesNode is not null) + { + if (closesNode.Id == goal.Id) return new(true, "Robot đang đứng tại điểm đích") { Data = [goal] }; + RobotNode = GetClosesNode(closesNode, goal, theta); + } + else + { + var closesEdge = GetClosesEdge(x, y, maxDistanceToEdge, cancellationToken); + if (closesEdge is null) + { + return new(false, "Robot đang quá xa tuyến đường"); + } + + var startNodeForward = Nodes.FirstOrDefault(p => p.Id == closesEdge.StartNodeId); + var startNodeBackward = Nodes.FirstOrDefault(p => p.Id == closesEdge.EndNodeId); + if (startNodeForward is null || startNodeBackward is null) + { + return new(false, "Dữ liệu lỗi: điểm chặn của edge gần nhất không tồn tại"); + } + if (startNodeForward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Backward)) + { + Path.Add(startNodeBackward); + Path.Add(startNodeForward); + return new(true) { Data = Path }; + } + if (startNodeBackward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Forward)) + { + Path.Add(startNodeForward); + Path.Add(startNodeBackward); + return new(true) { Data = Path }; + } + RobotNode.NegativeNodes.AddRange(GetClosesEdge(closesEdge, goal, startNodeForward, startNodeBackward, RobotNode, theta)); + } + + if (RobotNode.NegativeNodes.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + + var path = Find(RobotNode, goal, goalDirection, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + path.Reverse(); + foreach (var node in path) + { + if (node.Id == path.First().Id) + { + Path.Add(new() + { + Id = node.Id, + Name = node.Name, + X = node.X, + Y = node.Y, + Direction = MapCompute.GetNodeDirection(node.Direction), + }); + continue; + } + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + nodedb.Direction = MapCompute.GetNodeDirection(node.Direction); + Path.Add(nodedb); + } + return new(true) { Data = Path }; + } + + public MessageResult> PlanningWithGoalAngle(double x, double y, double theta, NodeDto goal, double maxDistanceToEdge, double maxDistanceToNode, CancellationToken? cancellationToken = null) + { + var Path = new List(); + + SSEAStarNode RobotNode = new() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + Name = "RobotCurrentNode", + }; + var closesNode = GetOnNode(x, y, maxDistanceToNode); + if (closesNode is not null) + { + if (closesNode.Id == goal.Id) return new(true, "Robot đang đứng tại điểm đích") { Data = [goal] }; + RobotNode = GetClosesNode(closesNode, goal, theta); + } + else + { + var closesEdge = GetClosesEdge(x, y, maxDistanceToEdge, cancellationToken); + if (closesEdge is null) + { + return new(false, "Robot đang quá xa tuyến đường"); + } + + var startNodeForward = Nodes.FirstOrDefault(p => p.Id == closesEdge.StartNodeId); + var startNodeBackward = Nodes.FirstOrDefault(p => p.Id == closesEdge.EndNodeId); + if (startNodeForward is null || startNodeBackward is null) + { + return new(false, "Dữ liệu lỗi: điểm chặn của edge gần nhất không tồn tại"); + } + if (startNodeForward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Backward)) + { + Path.Add(startNodeBackward); + Path.Add(startNodeForward); + return new(true) { Data = Path }; + } + if (startNodeBackward.Id == goal.Id && (closesEdge.DirectionAllowed == DirectionAllowed.Both || closesEdge.DirectionAllowed == DirectionAllowed.Forward)) + { + Path.Add(startNodeForward); + Path.Add(startNodeBackward); + return new(true) { Data = Path }; + } + RobotNode.NegativeNodes.AddRange(GetClosesEdge(closesEdge, goal, startNodeForward, startNodeBackward, RobotNode, theta)); + } + + if (RobotNode.NegativeNodes.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + + var path = Find(RobotNode, goal, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}]"); + path.Reverse(); + foreach (var node in path) + { + if (node.Id == path.First().Id) + { + Path.Add(new() + { + Id = node.Id, + Name = node.Name, + X = node.X, + Y = node.Y, + Direction = MapCompute.GetNodeDirection(node.Direction), + }); + continue; + } + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + nodedb.Direction = MapCompute.GetNodeDirection(node.Direction); + Path.Add(nodedb); + } + return new(true) { Data = Path }; + } + + public MessageResult PlanningWithFinalDirection(NodeDto startNode, double theta, NodeDto goal, RobotDirection goalDirection, CancellationToken? cancellationToken = null) + { + var Path = new List(); + SSEAStarNode RobotNode = GetClosesNode(startNode, goal, theta); + var path = Find(RobotNode, goal, goalDirection, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + path.Reverse(); + foreach (var node in path) + { + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + nodedb.Direction = MapCompute.GetNodeDirection(node.Direction); + Path.Add(nodedb); + } + return new(true) { Data = [.. Path] }; + } + + public MessageResult PlanningWithGoalAngle(NodeDto startNode, double theta, NodeDto goal, CancellationToken? cancellationToken = null) + { + var Path = new List(); + SSEAStarNode RobotNode = GetClosesNode(startNode, goal, theta); + var path = Find(RobotNode, goal, cancellationToken); + if (cancellationToken is not null && cancellationToken.Value.IsCancellationRequested) return new(false, "Yêu cầu hủy bỏ"); + if (path is null || path.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + path.Reverse(); + foreach (var node in path) + { + var nodedb = Nodes.FirstOrDefault(p => p.Id == node.Id); + if (nodedb is null) return new(false, "Dữ liệu bản đồ có lỗi"); + nodedb.Direction = MapCompute.GetNodeDirection(node.Direction); + Path.Add(nodedb); + } + return new(true) { Data = [.. Path] }; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/IPathPlanner.cs b/RobotNet.RobotManager/Services/Planner/IPathPlanner.cs new file mode 100644 index 0000000..b5581a5 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/IPathPlanner.cs @@ -0,0 +1,20 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner; + +public interface IPathPlanner +{ + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(double x, double y, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(double x, double y, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null); + + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null); + MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null); + void SetData(NodeDto[] nodes, EdgeDto[] edges); + void SetOptions(PathPlanningOptions options); +} diff --git a/RobotNet.RobotManager/Services/Planner/IPathPlannerManager.cs b/RobotNet.RobotManager/Services/Planner/IPathPlannerManager.cs new file mode 100644 index 0000000..16e0bfd --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/IPathPlannerManager.cs @@ -0,0 +1,8 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Planner; + +public interface IPathPlannerManager +{ + IPathPlanner GetPathPlanningService(NavigationType type); +} diff --git a/RobotNet.RobotManager/Services/Planner/OmniDrive/OmniDrivePathPlanner.cs b/RobotNet.RobotManager/Services/Planner/OmniDrive/OmniDrivePathPlanner.cs new file mode 100644 index 0000000..d752e79 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/OmniDrive/OmniDrivePathPlanner.cs @@ -0,0 +1,265 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Services.Planner.AStar; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.OmniDrive; + +public class OmniDrivePathPlanner : IPathPlanner +{ + private List Nodes = []; + private List Edges = []; + private const double ReverseDirectionAngleDegree = 89; + private const double Ratio = 0.1; + + private readonly PathPlanningOptions Options = new() + { + LimitDistanceToEdge = 1, + LimitDistanceToNode = 0.3, + ResolutionSplit = 0.1, + }; + + public void SetData(NodeDto[] nodes, EdgeDto[] edges) + { + Nodes = [.. nodes]; + Edges = [.. edges]; + } + + public void SetOptions(PathPlanningOptions options) + { + Options.LimitDistanceToNode = options.LimitDistanceToNode; + Options.LimitDistanceToEdge = options.LimitDistanceToEdge; + Options.ResolutionSplit = options.ResolutionSplit; + } + + public static RobotDirection[] GetRobotDirectionInPath(RobotDirection currentDirection, List nodeplanning, List edgePlanning) + { + RobotDirection[] RobotDirectionInNode = new RobotDirection[nodeplanning.Count]; + if (nodeplanning.Count > 0) RobotDirectionInNode[0] = currentDirection; + if (nodeplanning.Count > 2) + { + for (int i = 1; i < nodeplanning.Count - 1; i++) + { + NodeDto startNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i - 1], edgePlanning[i - 1], Ratio); + NodeDto endNode = MapEditorHelper.GetNearByNode(nodeplanning[i], nodeplanning[i + 1], edgePlanning[i], Ratio); ; + var angle = MapEditorHelper.GetAngle(nodeplanning[i], startNode, endNode); + if (angle < ReverseDirectionAngleDegree) RobotDirectionInNode[i] = RobotDirectionInNode[i - 1] == RobotDirection.FORWARD ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else RobotDirectionInNode[i] = RobotDirectionInNode[i - 1]; + } + } + if (nodeplanning.Count > 1) RobotDirectionInNode[^1] = RobotDirectionInNode[^2]; + return RobotDirectionInNode; + } + + public static RobotDirection GetRobotDirection(NodeDto currentNode, NodeDto nearNode, EdgeDto edge, double robotInNodeAngle, bool isReverse) + { + NodeDto NearNode = MapEditorHelper.GetNearByNode(currentNode, nearNode, edge, Ratio); + + var RobotNearNode = new NodeDto() + { + X = currentNode.X + Math.Cos(robotInNodeAngle * Math.PI / 180), + Y = currentNode.Y + Math.Sin(robotInNodeAngle * Math.PI / 180), + }; + var angle = MapEditorHelper.GetAngle(currentNode, NearNode, RobotNearNode); + if (angle > ReverseDirectionAngleDegree) return isReverse ? RobotDirection.BACKWARD : RobotDirection.FORWARD; + else return isReverse ? RobotDirection.FORWARD : RobotDirection.BACKWARD; + } + + private EdgeDto[] GetEdgesPlanning(NodeDto[] nodes) + { + var EdgesPlanning = new List(); + for (int i = 0; i < nodes.Length - 1; i++) + { + var edge = Edges.FirstOrDefault(e => e.StartNodeId == nodes[i].Id && e.EndNodeId == nodes[i + 1].Id || + e.EndNodeId == nodes[i].Id && e.StartNodeId == nodes[i + 1].Id); + if (edge is null) + { + if (i != 0) return []; + EdgesPlanning.Add(new EdgeDto() + { + Id = Guid.NewGuid(), + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + }); + continue; + } + bool isReverse = nodes[i].Id != edge.StartNodeId && edge.TrajectoryDegree == TrajectoryDegree.Three; + EdgesPlanning.Add(new() + { + Id = edge.Id, + StartNodeId = nodes[i].Id, + EndNodeId = nodes[i + 1].Id, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = isReverse ? edge.ControlPoint2X : edge.ControlPoint1X, + ControlPoint1Y = isReverse ? edge.ControlPoint2Y : edge.ControlPoint1Y, + ControlPoint2X = isReverse ? edge.ControlPoint1X : edge.ControlPoint2X, + ControlPoint2Y = isReverse ? edge.ControlPoint1Y : edge.ControlPoint2Y + }); + } + return [.. EdgesPlanning]; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(x, + y, + new NodeDto() { Id = goal.Id, Name = goal.Name, X = goal.X, Y = goal.Y }, + Options.LimitDistanceToEdge, + Options.LimitDistanceToNode, + cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Count < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{x}, {y}, {theta}]"); + if (Path.Data.Count == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, true); + var FinalDirection = GetRobotDirectionInPath(CurrenDirection, Path.Data, [.. edgeplannings]); + foreach (var item in Path.Data) + { + item.Direction = MapCompute.GetNodeDirection(FinalDirection[Path.Data.IndexOf(item)]); + } + + return new(true) + { + Data = ([.. Path.Data], [.. edgeplannings]) + }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(double x, double y, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, false); + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(double x, double y, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(startDiretion) == basicPath.Data.Nodes[0].Direction || startDiretion == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(double x, double y, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(x, y, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction || goalDirection == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanning(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var startNode = Nodes.FirstOrDefault(n => n.Id == startNodeId); + if (startNode is null) return new(false, $"Điểm bắt đầu {startNodeId} không tồn tại trong map"); + + var AStarPathPlanner = new AStarPathPlanner(Nodes, Edges); + var Path = AStarPathPlanner.Planning(startNode, goal, cancellationToken); + if (!Path.IsSuccess) return new(false, Path.Message); + if (Path.Data is null || Path.Data.Length < 1) return new(false, $"Đường đi đến {goal.Name} - {goal.Id} không tồn tại từ [{startNode.Name} - {startNode.Id}]"); + if (Path.Data.Length == 1) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + var edgeplannings = GetEdgesPlanning([.. Path.Data]); + + RobotDirection CurrenDirection = GetRobotDirection(Path.Data[0], Path.Data[1], edgeplannings[0], theta, true); + var FinalDirection = GetRobotDirectionInPath(CurrenDirection, [.. Path.Data], [.. edgeplannings]); + for (int i = 0; i < Path.Data.Length; i++) + { + Path.Data[i].Direction = MapCompute.GetNodeDirection(FinalDirection[i]); + } + + return new(true) + { + Data = ([.. Path.Data], [.. edgeplannings]) + }; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithAngle(Guid startNodeId, double theta, Guid goalId, CancellationToken? cancellationToken = null) + { + var goal = Nodes.FirstOrDefault(n => n.Id == goalId); + if (goal is null) return new(false, $"Đích đến {goalId} không tồn tại trong map"); + + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + RobotDirection goalDirection = GetRobotDirection(basicPath.Data.Nodes[^1], basicPath.Data.Nodes[^2], basicPath.Data.Edges[^1], goal.Theta, false); + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithStartDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection startDiretion = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(startDiretion) == basicPath.Data.Nodes[0].Direction || startDiretion == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } + + public MessageResult<(NodeDto[] Nodes, EdgeDto[] Edges)> PathPlanningWithFinalDirection(Guid startNodeId, double theta, Guid goalId, RobotDirection goalDirection = RobotDirection.NONE, CancellationToken? cancellationToken = null) + { + var basicPath = PathPlanning(startNodeId, theta, goalId, cancellationToken); + if (!basicPath.IsSuccess) return basicPath; + if (basicPath.Data.Nodes.Length < 2) return new(true, "Robot đang đứng ở điểm đích") { Data = ([], []) }; + + if (MapCompute.GetNodeDirection(goalDirection) == basicPath.Data.Nodes[^1].Direction || goalDirection == RobotDirection.NONE) return basicPath; + foreach (var node in basicPath.Data.Nodes) + { + node.Direction = node.Direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD; + } + + return basicPath; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/PathPlannerManager.cs b/RobotNet.RobotManager/Services/Planner/PathPlannerManager.cs new file mode 100644 index 0000000..fe4c7fa --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/PathPlannerManager.cs @@ -0,0 +1,19 @@ +using RobotNet.RobotManager.Services.Planner.Differential; +using RobotNet.RobotManager.Services.Planner.ForkliftV2; +using RobotNet.RobotManager.Services.Planner.OmniDrive; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Planner; + +public class PathPlannerManager : IPathPlannerManager +{ + public IPathPlanner GetPathPlanningService(NavigationType type) + { + return type switch + { + NavigationType.Forklift => new ForkLiftPathPlannerV2(), + NavigationType.OmniDrive => new OmniDrivePathPlanner(), + _ => new DifferentialPathPlanner(), + }; + } +} diff --git a/RobotNet.RobotManager/Services/Planner/PathPlanningOptions.cs b/RobotNet.RobotManager/Services/Planner/PathPlanningOptions.cs new file mode 100644 index 0000000..f1fc7a5 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/PathPlanningOptions.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotManager.Services.Planner; + +public class PathPlanningOptions +{ + public double LimitDistanceToEdge { get; set; } + public double LimitDistanceToNode { get; set; } + public double ResolutionSplit { get; set; } +} diff --git a/RobotNet.RobotManager/Services/Planner/PriorityQueue.cs b/RobotNet.RobotManager/Services/Planner/PriorityQueue.cs new file mode 100644 index 0000000..1dd0445 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/PriorityQueue.cs @@ -0,0 +1,23 @@ +namespace RobotNet.RobotManager.Services.Planner; + +public class PriorityQueue(Comparison comparison) +{ + public List Items => items; + private readonly List items = []; + + public void Enqueue(T item) + { + items.Add(item); + items.Sort(comparison); + } + + public T Dequeue() + { + if (items.Count == 0) throw new InvalidOperationException("Queue is empty"); + var item = items[0]; + items.RemoveAt(0); + return item; + } + + public int Count => items.Count; +} diff --git a/RobotNet.RobotManager/Services/Planner/Space/KDTree.cs b/RobotNet.RobotManager/Services/Planner/Space/KDTree.cs new file mode 100644 index 0000000..62fb1c3 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Space/KDTree.cs @@ -0,0 +1,61 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.RobotManager.Services.Planner.Space; + +public class KDTree(List nodes) +{ + private readonly KDTreeNode? Root = BuildTree(nodes, 0); + + private static KDTreeNode? BuildTree(List nodes, int depth) + { + if (nodes.Count == 0) return null; + int axis = depth % 2; + nodes.Sort((a, b) => axis == 0 ? a.X.CompareTo(b.X) : a.Y.CompareTo(b.Y)); + int median = nodes.Count / 2; + + return new KDTreeNode + { + Node = nodes[median], + Axis = axis, + Left = BuildTree(nodes.GetRange(0, median), depth + 1), + Right = BuildTree(nodes.GetRange(median + 1, nodes.Count - median - 1), depth + 1) + }; + } + + public NodeDto? FindNearest(double x, double y, double limitDistance) + { + if (Root is null) return null; + var (node, dist) = Nearest(Root, x, y, null, double.MaxValue); + return dist <= limitDistance ? node : null; + } + + private static (NodeDto?, double) Nearest(KDTreeNode? node, double x, double y, NodeDto? best, double bestDist) + { + if (node == null) return (best, bestDist); + + double d = Math.Sqrt(Math.Pow(node.Node.X - x, 2) + Math.Pow(node.Node.Y - y, 2)); + if (d < bestDist) + { + best = node.Node; + bestDist = d; + } + + double delta = node.Axis == 0 ? x - node.Node.X : y - node.Node.Y; + KDTreeNode? nearSide = delta < 0 ? node.Left : node.Right; + KDTreeNode? farSide = delta < 0 ? node.Right : node.Left; + + (best, bestDist) = Nearest(nearSide, x, y, best, bestDist); + if (Math.Abs(delta) < bestDist) + (best, bestDist) = Nearest(farSide, x, y, best, bestDist); + + return (best, bestDist); + } +} + +public class KDTreeNode +{ + public NodeDto Node { get; set; } = new(); + public KDTreeNode? Left { get; set; } + public KDTreeNode? Right { get; set; } + public int Axis { get; set; } // 0 for X, 1 for Y +} diff --git a/RobotNet.RobotManager/Services/Planner/Space/MapCompute.cs b/RobotNet.RobotManager/Services/Planner/Space/MapCompute.cs new file mode 100644 index 0000000..2e1a3a8 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Space/MapCompute.cs @@ -0,0 +1,86 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Planner.Space; + +public class MapCompute +{ + public static double DistanceToCurveEdge(double x, double y, EdgeDto edge, NodeDto startNode, NodeDto endNode) + { + double dMin = Math.Sqrt(Math.Pow(x - startNode.X, 2) + Math.Pow(y - startNode.Y, 2)); + var length = MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }); + double step = 0.1 / (length == 0 ? 0.1 : length); + + for (double t = 0; t <= 1; t += step) + { + (double curveX, double curveY) = MapEditorHelper.Curve(t, new() + { + TrajectoryDegree = edge.TrajectoryDegree, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y= edge.ControlPoint2Y, + X1 = startNode.X, + Y1 = startNode.Y, + X2 = endNode.X, + Y2 = endNode.Y, + }); + double d = Math.Sqrt(Math.Pow(x - curveX, 2) + Math.Pow(y - curveY, 2)); + if (d < dMin) dMin = d; + } + + return dMin; + } + + public static MessageResult DistanceToEdge(double x, double y, EdgeDto edge, NodeDto startNode, NodeDto endNode) + { + if (edge.TrajectoryDegree == TrajectoryDegree.One) + { + double time = 0; + var edgeLengthSquared = Math.Pow(startNode.X - endNode.X, 2) + Math.Pow(startNode.Y - endNode.Y, 2); + if (edgeLengthSquared > 0) + { + time = Math.Max(0, Math.Min(1, ((x - startNode.X) * (endNode.X - startNode.X) + (y - startNode.Y) * (endNode.Y - startNode.Y)) / edgeLengthSquared)); + } + + double nearestX = startNode.X + time * (endNode.X - startNode.X); + double nearestY = startNode.Y + time * (endNode.Y - startNode.Y); + + return new(true) { Data = Math.Sqrt(Math.Pow(x - nearestX, 2) + Math.Pow(y - nearestY, 2)) }; + } + else + { + return new(true) { Data = DistanceToCurveEdge(x, y, edge, startNode, endNode) }; + } + } + + public static Direction GetNodeDirection(RobotDirection robotDirection) => + robotDirection switch + { + RobotDirection.FORWARD => Direction.FORWARD, + RobotDirection.BACKWARD => Direction.BACKWARD, + _ => Direction.NONE + }; + + public static RobotDirection GetRobotDirection(Direction nodeDirection) => + nodeDirection switch + { + Direction.FORWARD => RobotDirection.FORWARD, + Direction.BACKWARD => RobotDirection.BACKWARD, + _ => RobotDirection.NONE + }; +} diff --git a/RobotNet.RobotManager/Services/Planner/Space/RTree.cs b/RobotNet.RobotManager/Services/Planner/Space/RTree.cs new file mode 100644 index 0000000..3fb4af7 --- /dev/null +++ b/RobotNet.RobotManager/Services/Planner/Space/RTree.cs @@ -0,0 +1,206 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.RobotManager.Services.Planner.Space; + +public class Rectangle(double minX, double minY, double maxX, double maxY) +{ + public double MinX { get; set; } = minX; + public double MinY { get; set; } = minY; + public double MaxX { get; set; } = maxX; + public double MaxY { get; set; } = maxY; + + public double DistanceToPoint(double x, double y) + { + double dx = Math.Max(Math.Max(MinX - x, 0), x - MaxX); + double dy = Math.Max(Math.Max(MinY - y, 0), y - MaxY); + return Math.Sqrt(dx * dx + dy * dy); + } + + public bool Contains(double x, double y) + { + return x >= MinX && x <= MaxX && y >= MinY && y <= MaxY; + } +} + +public class RTreeNode +{ + public Rectangle Bounds { get; set; } = new Rectangle(0, 0, 0, 0); + public List Children { get; set; } = []; + public List<(EdgeDto Edge, Rectangle Rect)> Entries { get; set; } = []; + public bool IsLeaf => Children.Count == 0; +} + +public class RTree +{ + private readonly RTreeNode Root; + private const int MaxEntries = 4; + private readonly List Nodes; + + public RTree(List nodes, List edges) + { + Nodes = nodes; + Root = new RTreeNode(); + foreach (var edge in edges) + { + var rect = CalculateMBR(edge); + Insert(Root, (edge, rect)); + } + } + + private Rectangle CalculateMBR(EdgeDto edge) + { + var startNode = Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode == null || endNode == null) return new Rectangle(0, 0, 0, 0); + + double minX = Math.Min(startNode.X, endNode.X); + double maxX = Math.Max(startNode.X, endNode.X); + double minY = Math.Min(startNode.Y, endNode.Y); + double maxY = Math.Max(startNode.Y, endNode.Y); + + // Mở rộng MBR nếu edge là đường cong + if (edge.TrajectoryDegree != TrajectoryDegree.One) + { + minX = Math.Min(minX, Math.Min(edge.ControlPoint1X, edge.ControlPoint2X)); + maxX = Math.Max(maxX, Math.Max(edge.ControlPoint1X, edge.ControlPoint2X)); + minY = Math.Min(minY, Math.Min(edge.ControlPoint1Y, edge.ControlPoint2Y)); + maxY = Math.Max(maxY, Math.Max(edge.ControlPoint1Y, edge.ControlPoint2Y)); + } + + return new Rectangle(minX, minY, maxX, maxY); + } + + private static void Insert(RTreeNode node, (EdgeDto Edge, Rectangle Rect) entry) + { + if (node.IsLeaf) + { + node.Entries.Add(entry); + if (node.Entries.Count > MaxEntries) + { + SplitNode(node); + } + } + else + { + var bestChild = ChooseBestChild(node, entry.Rect); + Insert(bestChild, entry); + AdjustBounds(bestChild); + } + + node.Bounds.MinX = Math.Min(node.Bounds.MinX, entry.Rect.MinX); + node.Bounds.MinY = Math.Min(node.Bounds.MinY, entry.Rect.MinY); + node.Bounds.MaxX = Math.Max(node.Bounds.MaxX, entry.Rect.MaxX); + node.Bounds.MaxY = Math.Max(node.Bounds.MaxY, entry.Rect.MaxY); + } + + private static RTreeNode ChooseBestChild(RTreeNode node, Rectangle rect) + { + RTreeNode best = node.Children[0]; + double minEnlargement = CalculateEnlargement(best.Bounds, rect); + foreach (var child in node.Children.Skip(1)) + { + double enlargement = CalculateEnlargement(child.Bounds, rect); + if (enlargement < minEnlargement) + { + minEnlargement = enlargement; + best = child; + } + } + return best; + } + + private static double CalculateEnlargement(Rectangle bounds, Rectangle rect) + { + double newArea = (Math.Max(bounds.MaxX, rect.MaxX) - Math.Min(bounds.MinX, rect.MinX)) * + (Math.Max(bounds.MaxY, rect.MaxY) - Math.Min(bounds.MinY, rect.MinY)); + double oldArea = (bounds.MaxX - bounds.MinX) * (bounds.MaxY - bounds.MinY); + return newArea - oldArea; + } + + private static void SplitNode(RTreeNode node) + { + var (group1, group2) = SplitEntries(node.Entries); + node.Children.Add(new RTreeNode { Entries = group1 }); + node.Children.Add(new RTreeNode { Entries = group2 }); + node.Entries.Clear(); + foreach (var child in node.Children) + { + child.Bounds = CalculateBounds(child.Entries); + } + } + + private static (List<(EdgeDto, Rectangle)>, List<(EdgeDto, Rectangle)>) SplitEntries(List<(EdgeDto Edge, Rectangle Rect)> entries) + { + entries.Sort((a, b) => a.Rect.MinX.CompareTo(b.Rect.MinX)); + int mid = entries.Count / 2; + return (entries.GetRange(0, mid), entries.GetRange(mid, entries.Count - mid)); + } + + private static Rectangle CalculateBounds(List<(EdgeDto Edge, Rectangle Rect)> entries) + { + if (entries.Count == 0) return new(0, 0, 0, 0); + var first = entries[0].Rect; + double minX = first.MinX, minY = first.MinY, maxX = first.MaxX, maxY = first.MaxY; + foreach (var (Edge, Rect) in entries.Skip(1)) + { + minX = Math.Min(minX, Rect.MinX); + minY = Math.Min(minY, Rect.MinY); + maxX = Math.Max(maxX, Rect.MaxX); + maxY = Math.Max(maxY, Rect.MaxY); + } + return new Rectangle(minX, minY, maxX, maxY); + } + + private static void AdjustBounds(RTreeNode node) + { + if (node.IsLeaf) + { + node.Bounds = CalculateBounds(node.Entries); + } + else + { + node.Bounds = CalculateBounds(node.Children.SelectMany(c => c.Entries).ToList()); + } + } + + public EdgeDto? FindNearest(double x, double y, double limitDistance) + { + double minDistance = double.MaxValue; + EdgeDto? nearestEdge = null; + SearchNearest(Root, x, y, ref minDistance, ref nearestEdge); + return minDistance <= limitDistance ? nearestEdge : null; + } + + private void SearchNearest(RTreeNode node, double x, double y, ref double minDistance, ref EdgeDto? nearestEdge) + { + if (node.IsLeaf) + { + foreach (var (edge, rect) in node.Entries) + { + var startNode = Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + if (startNode == null || endNode == null) continue; + + var distanceResult = MapCompute.DistanceToEdge(x, y, edge, startNode, endNode); + if (distanceResult.IsSuccess && distanceResult.Data < minDistance) + { + minDistance = distanceResult.Data; + nearestEdge = edge; + } + } + } + else + { + var sortedChildren = node.Children.OrderBy(c => c.Bounds.DistanceToPoint(x, y)); + foreach (var child in sortedChildren) + { + if (child.Bounds.DistanceToPoint(x, y) < minDistance) + { + SearchNearest(child, x, y, ref minDistance, ref nearestEdge); + } + } + } + } +} \ No newline at end of file diff --git a/RobotNet.RobotManager/Services/Robot/RobotController.cs b/RobotNet.RobotManager/Services/Robot/RobotController.cs new file mode 100644 index 0000000..8e9f2ce --- /dev/null +++ b/RobotNet.RobotManager/Services/Robot/RobotController.cs @@ -0,0 +1,750 @@ +using Microsoft.EntityFrameworkCore; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.RobotShares.VDA5050; +using RobotNet.RobotShares.VDA5050.Factsheet; +using RobotNet.RobotShares.VDA5050.FactsheetExtend; +using RobotNet.RobotShares.VDA5050.Order; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Type; +using RobotNet.RobotShares.VDA5050.Visualization; +using RobotNet.Shares; +using System.Text.Json; +using Action = RobotNet.RobotShares.VDA5050.InstantAction.Action; +using NodePosition = RobotNet.RobotShares.VDA5050.Order.NodePosition; + +namespace RobotNet.RobotManager.Services.Robot; + +public class RobotController(string robotid, + string Manufacturer, + string Version, + NavigationType NavigationType, + IServiceProvider ServiceProvider, + Func FuncPub) : IRobotController, IDisposable +{ + public string SerialNumber { get; } = robotid; + public bool IsOnline { get; set; } + public bool IsWorking => (RobotOrder is not null && RobotOrder.IsProcessing) || StateMsg.NodeStates.Length != 0 || StateMsg.EdgeStates.Length != 0; + public string State => GetState(); + public string[] CurrentZones => RobotOrder is null ? [] : [.. RobotOrder.CurrentZones.Select(z => z.Name)]; + public RobotOrderDto OrderState => GetOrderState(); + public RobotActionDto[] ActionStates => GetActionsState(); + public AutoResetEvent RobotUpdated { get; set; } = new(false); + public StateMsg StateMsg { get; set; } = new(); + public VisualizationMsg VisualizationMsg { get; set; } = new(); + public FactSheetMsg FactSheetMsg { get; set; } = new(); + public FactsheetExtendMsg FactsheetExtendMsg { get; set; } = new(); + public NavigationPathEdge[] BasePath => RobotOrder is null ? [] : RobotOrder.BasePath; + public NavigationPathEdge[] FullPath => RobotOrder is null ? [] : RobotOrder.FullPath; + + private readonly LoggerController Logger = ServiceProvider.GetRequiredService>(); + private readonly TimeSpan WaittingRobotFeedbackTime = TimeSpan.FromSeconds(10); + private RobotOrder? RobotOrder; + + private CancellationTokenSource? CancelRandom; + + public void Log(string message, LogLevel level = LogLevel.Information) + { + switch (level) + { + case LogLevel.Trace: Logger.Trace($"{SerialNumber} - {message}"); break; + case LogLevel.Error: Logger.Error($"{SerialNumber} - {message}"); break; + case LogLevel.Warning: Logger.Warning($"{SerialNumber} - {message}"); break; + case LogLevel.Critical: Logger.Critical($"{SerialNumber} - {message}"); break; + case LogLevel.Information: Logger.Info($"{SerialNumber} - {message}"); break; + case LogLevel.Debug: Logger.Debug($"{SerialNumber} - {message}"); break; + default: Logger.Debug($"{SerialNumber} - {message}"); break; + } + } + + public async Task CancelOrder() + { + try + { + if (StateMsg.NodeStates.Length != 0 || StateMsg.EdgeStates.Length != 0) + { + Action cancelOrderAction = new() + { + ActionDescription = "Yêu cầu hủy nhiệm vụ hiện tại", + ActionParameters = [new RobotShares.VDA5050.InstantAction.ActionParameter() + { + Key = "ORDER_ID", + Value = StateMsg.OrderId, + }], + ActionType = ActionType.cancelOrder.ToString(), + BlockingType = RobotShares.VDA5050.InstantAction.BlockingType.NONE.ToString(), + }; + var pubAction = await InstantAction(cancelOrderAction, true); + if (!pubAction.IsSuccess) return new(false, pubAction.Message); + } + if (CancelRandom is not null && !CancelRandom.IsCancellationRequested) CancelRandom.Cancel(); + if (RobotOrder is not null && RobotOrder.IsProcessing) + { + CancellationTokenSource cancellationToken = new(); + cancellationToken.CancelAfter(TimeSpan.FromSeconds(10)); + var waitCancel = await RobotOrder.Cancel(cancellationToken.Token); + if (!waitCancel) return new(false, "Robot không kết thúc order"); + } + return new(true); + } + catch (Exception ex) + { + Logger.Warning($"{SerialNumber} Cancel Order logs: {ex.Message}"); + return new(false, $"{SerialNumber} Cancel Order logs: {ex.Message}"); + } + } + + public async Task CancelAction() + { + try + { + Action cancelOrderAction = new() + { + ActionDescription = "Yêu cầu hủy Action hiện tại", + ActionParameters = [new RobotShares.VDA5050.InstantAction.ActionParameter() + { + Key = "ORDER_ID", + Value = SerialNumber, + }], + ActionType = ActionType.cancelOrder.ToString(), + BlockingType = RobotShares.VDA5050.InstantAction.BlockingType.NONE.ToString(), + }; + var pubAction = await InstantAction(cancelOrderAction, true); + return new(pubAction.IsSuccess, pubAction.Message); + } + catch (Exception ex) + { + Logger.Warning($"{SerialNumber} Cancel Action logs: {ex.Message}"); + return new(false, $"{SerialNumber} Cancel Action logs: {ex.Message}"); + } + } + + public void Initialize(double x, double y, double theta) + { + throw new NotImplementedException(); + } + + public async Task> InstantAction(Action action, bool waittingFisished) + { + CancellationTokenSource CancellationToken = new(); + CancellationToken.CancelAfter(WaittingRobotFeedbackTime); + bool IsFeedback = false; + int RepeatCounter = 0; + try + { + action.ActionId = Guid.NewGuid().ToString(); + var instantActionsMsg = new RobotNet.RobotShares.VDA5050.InstantAction.InstantActionsMsg() + { + HeaderId = 1, + Manufacturer = Manufacturer, + Version = Version, + Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + SerialNumber = SerialNumber, + Actions = [action], + }; + var pubInstantAction = FuncPub.Invoke(VDA5050Topic.InstantActions, JsonSerializer.Serialize(instantActionsMsg, JsonOptionExtends.Write)); + if (!pubInstantAction) + { + string msg = $"Gửi Action xuống cho robot không thành công: {action.ActionType}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + while (!IsFeedback) + { + if (StateMsg is not null && StateMsg.ActionStates is not null) + { + var actionstate = StateMsg.ActionStates.FirstOrDefault(ac => ac.ActionId == action.ActionId); + if (actionstate is not null) + { + IsFeedback = true; + break; + } + } + if(CancellationToken.IsCancellationRequested) + { + if (RepeatCounter++ == 0) + { + await Task.Delay(1000); + CancellationToken = new(); + CancellationToken.CancelAfter(WaittingRobotFeedbackTime); + pubInstantAction = FuncPub.Invoke(VDA5050Topic.InstantActions, JsonSerializer.Serialize(instantActionsMsg, JsonOptionExtends.Write)); + if (!pubInstantAction) + { + string msg = $"Gửi Action xuống cho robot không thành công: {action.ActionType}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + } + else return new(false, $"Robot không đồng ý thực hiện action: {action.ActionType}"); + } + await Task.Delay(500); + } + if (waittingFisished) + { + while (!CancellationToken.IsCancellationRequested) + { + if (StateMsg is not null) + { + if (StateMsg.ActionStates is not null) + { + var actionCancelOrder = StateMsg.ActionStates.FirstOrDefault(a => a.ActionId == action.ActionId); + if (actionCancelOrder is not null) + { + if (ActionFinish(actionCancelOrder.ActionStatus)) return new(true); + if (ActionFailed(actionCancelOrder.ActionStatus)) + { + string msg = $"Robot trả về thực hiện action lỗi: {action.ActionType}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + } + } + if (GetErrorLevel() != ErrorLevel.NONE) + { + var errorStr = StateMsg.Errors.Select(error => error.ErrorType).ToArray(); + string msg = $"Robot xảy ra lỗi sau khi gọi thực hiện action {action.ActionType}"; + Log($"{msg} - {string.Join(",", errorStr)}", LogLevel.Warning); + return new(false, msg); + } + } + if (CancellationToken.IsCancellationRequested) return new(false, "Robot đang thực hiện Action nhưng đã quá thời gian timeout 10s"); + await Task.Delay(500); + } + } + if (CancellationToken.IsCancellationRequested) throw new Exception(); + return new(true) { Data = action.ActionId }; + } + catch (Exception ex) + { + string msg = $"Có lỗi xảy ra trong quá trình gửi action : {ex.Message}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + } + + public MessageResult MoveStraight(double x, double y) + { + return new(false, "Chức năng này hiện không khả dụng"); + } + + public async Task MoveToNode(string goalName, IDictionary>? actions = null, double? lastAngle = null) + { + try + { + if (IsWorking) return new(false, "Robot đang thực hiện nhiệm vụ"); + + using var scope = ServiceProvider.CreateScope(); + var PathPlanner = scope.ServiceProvider.GetRequiredService(); + var robotDb = scope.ServiceProvider.GetRequiredService(); + var MapManager = scope.ServiceProvider.GetRequiredService(); + var TrafficManager = scope.ServiceProvider.GetRequiredService(); + var TrafficACS = scope.ServiceProvider.GetRequiredService(); + var orderLogger = scope.ServiceProvider.GetRequiredService>(); + + var robot = await robotDb.Robots.FirstOrDefaultAsync(r => r.RobotId == SerialNumber) ?? + throw new Exception($"Không tìm thấy robot {SerialNumber} trong kho dữ liệu"); + var robotModel = await robotDb.RobotModels.FirstOrDefaultAsync(r => r.Id == robot.ModelId) ?? + throw new Exception($"Không tìm thấy robot model {robot.ModelId} trong kho dữ liệu"); + + var path = await PathPlanner.Planning(VisualizationMsg.AgvPosition.X, VisualizationMsg.AgvPosition.Y, VisualizationMsg.AgvPosition.Theta, NavigationType, robot.MapId, goalName); + if (!path.IsSuccess) return new(false, path.Message); + if(path.IsSuccess && path.Data.Nodes.Length < 2) + { + RobotOrder?.CreateComledted(); + return new(true, ""); + } + if (path.Data.Nodes is null || path.Data.Edges is null || path.Data.Edges.Length == 0 || path.Data.Nodes.Length < 2) + return new(false, $"Đường dẫn tới đích {goalName} từ [{VisualizationMsg.AgvPosition.X} - {VisualizationMsg.AgvPosition.Y} - {VisualizationMsg.AgvPosition.Theta}] không tồn tại"); + + if (lastAngle != null) + { + path.Data.Nodes[^1].Theta = lastAngle.Value; + } + + var mapData = await MapManager.GetMapData(robot.MapId); + if (!mapData.IsSuccess) return new(false, mapData.Message); + if (mapData is null || mapData.Data is null) return new(false, "Không thể tìm thấy bản đồ chứa robot."); + var nodeInZones = await PathPlanner.GetZones(robot.MapId, path.Data.Nodes); + if (!nodeInZones.IsSuccess) return new(false, mapData.Message); + var order = await Order([.. path.Data.Nodes], [.. path.Data.Edges], mapData.Data, actions, TrafficManager.Enable); + if (order.IsSuccess && order.Data is not null) + { + int counter = 0; + while (counter++ < 2) + { + var createAgent = TrafficManager.CreateAgent(robot.MapId, this, new() + { + + NavigationType = robotModel.NavigationType, + Length = robotModel.Length, + Width = robotModel.Width, + NavigationPointX = robotModel.OriginX, + NavigationPointY = robotModel.OriginY, + }, + [..path.Data.Nodes.Select(n => new TrafficNodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Direction = MapCompute.GetRobotDirection(n.Direction), + })], [..path.Data.Edges.Select(n => new TrafficEdgeDto() + { + Id = n.Id, + StartNodeId = n.StartNodeId, + EndNodeId = n.EndNodeId, + TrajectoryDegree = n.TrajectoryDegree, + ControlPoint1X = n.ControlPoint1X, + ControlPoint1Y = n.ControlPoint1Y, + ControlPoint2X = n.ControlPoint2X, + ControlPoint2Y = n.ControlPoint2Y, + })]); + if (createAgent.IsSuccess) break; + Logger.Warning($"{SerialNumber} - Không thể tạo traffic agent: {createAgent.Message}"); + if (counter > 1 && !createAgent.IsSuccess) + { + var cancel = await CancelOrder(); + if (!cancel.IsSuccess) Logger.Warning($"{SerialNumber} - Không thể hủy bỏ nhiệm vụ đã giao: {cancel.Message}"); + return new(false, $"Không thể tạo traffic agent: {createAgent.Message} - {(cancel.IsSuccess ? "Đã hủy order" : $"Không thể hủy order: {cancel.Message}")} "); + } + } + RobotOrder = new RobotOrder([.. path.Data.Nodes], [.. path.Data.Edges], nodeInZones.Data ?? [], actions ?? new Dictionary>(), + order.Data, this, NavigationType, TrafficManager, TrafficACS, MapManager, orderLogger, Order) + { + CurrentZones = RobotOrder is null ? [] : RobotOrder.CurrentZones, + }; + + return new(true); + } + else return new(order.IsSuccess, order.Message); + } + catch (Exception ex) + { + string msg = $"Có lỗi xảy ra trong quá trình gửi order to Node {goalName} : {ex.Message}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + } + + public MessageResult Rotate(double angle) + { + return new(false, "Chức năng này hiện không khả dụng"); + } + + private async Task Order(OrderMsg orderMsg) + { + CancellationTokenSource CancellationExitToken = new(); + CancellationExitToken.CancelAfter(WaittingRobotFeedbackTime); + try + { + var pubOrder = FuncPub.Invoke(VDA5050Topic.Order, JsonSerializer.Serialize(orderMsg, JsonOptionExtends.Write)); + if (!pubOrder) + { + string msg = $"Gửi Order xuống cho robot không thành công: {orderMsg.OrderId}"; + Log(msg, LogLevel.Warning); + return new(false, msg); + } + while (!CancellationExitToken.IsCancellationRequested) + { + if (StateMsg is not null) + { + if (StateMsg.OrderId == orderMsg.OrderId && IsWorking && StateMsg.OrderUpdateId == orderMsg.OrderUpdateId) break; + } + + await Task.Delay(500, CancellationExitToken.Token); + } + if (CancellationExitToken.IsCancellationRequested) throw new Exception(); + return new(true); + } + catch (Exception ex) + { + string msg = $"Có lỗi xảy ra trong quá trình gửi order : {ex.Message}"; + if (CancellationExitToken.IsCancellationRequested) msg = $"{SerialNumber} - Robot không đồng ý thực hiện order: {orderMsg.OrderId}"; + + Log(msg, LogLevel.Warning); + return new(false, msg); + } + } + + private static List ConvertAction(string actions, ActionDto[] mapAction) + { + List Actions = []; + var ActionIds = !string.IsNullOrEmpty(actions) ? JsonSerializer.Deserialize(actions) : []; + if (ActionIds is not null) + { + foreach (var actionid in ActionIds) + { + var actionDb = mapAction.FirstOrDefault(x => x.Id == actionid); + if (actionDb is not null) + { + var vdaAction = JsonSerializer.Deserialize(actionDb.Content, JsonOptionExtends.Read); + if (vdaAction is not null) Actions.Add(vdaAction); + } + } + } + return Actions; + } + + private static Edge? ConvertToOrderEdge(EdgeDto edge, MapDataDto map) + { + var startNode = map.Nodes.FirstOrDefault(n => n.Id == edge.StartNodeId); + var endNode = map.Nodes.FirstOrDefault(n => n.Id == edge.EndNodeId); + var dataEdge = map.Edges.FirstOrDefault(e => e.Id == edge.Id); + + if (startNode is null || endNode is null || dataEdge is null) return null; + + List ControlPoints = []; + ControlPoints.Add(new() + { + X = startNode.X, + Y = startNode.Y, + Weight = 1, + }); + if (dataEdge.TrajectoryDegree == TrajectoryDegree.Two) + { + ControlPoints.Add(new() + { + X = dataEdge.ControlPoint1X, + Y = dataEdge.ControlPoint1Y, + Weight = 1, + }); + } + else if (dataEdge.TrajectoryDegree == TrajectoryDegree.Three) + { + ControlPoint controlPoint1 = new(); + ControlPoint controlPoint2 = new(); + if (dataEdge.StartNodeId == edge.StartNodeId) + { + controlPoint1.X = dataEdge.ControlPoint1X; + controlPoint1.Y = dataEdge.ControlPoint1Y; + controlPoint1.Weight = 1; + controlPoint2.X = dataEdge.ControlPoint2X; + controlPoint2.Y = dataEdge.ControlPoint2Y; + controlPoint2.Weight = 1; + } + else + { + controlPoint2.X = dataEdge.ControlPoint1X; + controlPoint2.Y = dataEdge.ControlPoint1Y; + controlPoint2.Weight = 1; + controlPoint1.X = dataEdge.ControlPoint2X; + controlPoint1.Y = dataEdge.ControlPoint2Y; + controlPoint1.Weight = 1; + } + ControlPoints.Add(controlPoint1); + ControlPoints.Add(controlPoint2); + } + ControlPoints.Add(new() + { + X = endNode.X, + Y = endNode.Y, + Weight = 1, + }); + + var result = new Edge() + { + EdgeId = edge.Id.ToString(), + EdgeDescription = $"{startNode.Name} - {endNode.Name}", + EndNodeId = edge.EndNodeId.ToString(), + StartNodeId = edge.StartNodeId.ToString(), + SequenceId = 1, + Direction = dataEdge.DirectionAllowed.ToString(), + Orientation = 0, + Released = true, + Length = MapEditorHelper.GetEdgeLength(new() + { + X1 = startNode.X, + X2 = endNode.X, + Y1 = startNode.Y, + Y2 = endNode.Y, + TrajectoryDegree = dataEdge.TrajectoryDegree, + ControlPoint1X = dataEdge.ControlPoint1X, + ControlPoint1Y = dataEdge.ControlPoint1Y, + ControlPoint2X = dataEdge.ControlPoint2X, + ControlPoint2Y = dataEdge.ControlPoint2Y, + }), + MaxHeight = dataEdge.MaxHeight, + MaxRotationSpeed = dataEdge.MaxRotationSpeed, + OrientationType = "", + MaxSpeed = dataEdge.MaxSpeed, + MinHeight = dataEdge.MinHeight, + RotationAllowed = dataEdge.RotationAllowed, + Actions = [.. ConvertAction(dataEdge.Actions, [.. map.Actions])], + Trajectory = new() + { + Degree = dataEdge.TrajectoryDegree == TrajectoryDegree.One ? 1 : dataEdge.TrajectoryDegree == TrajectoryDegree.Two ? 2 : 3, + KnotVector = dataEdge.TrajectoryDegree == TrajectoryDegree.One ? [0, 0, 1, 1] : dataEdge.TrajectoryDegree == TrajectoryDegree.Two ? [0, 0, 0, 1, 1, 1] : [0, 0, 0, 0, 1, 1, 1, 1], + ControlPoints = [.. ControlPoints], + }, + }; + return result; + } + + public static void CreateOrderMsg(ref OrderMsg orderMsgTemplate, NodeDto[] nodes, EdgeDto[] edges, MapDataDto map, IDictionary> actions, NavigationType navigationType, bool trafficEnable) + { + if (nodes.Length <= 1 || edges.Length < 1) throw new ArgumentException("Dữ liệu đường đi không hợp lệ"); + List OrderNodes = []; + List OrderEdges = []; + for (int i = 0; i < nodes.Length; i++) + { + var nodeDb = map.Nodes.FirstOrDefault(n => n.Id == nodes[i].Id); + if (nodeDb is null) + { + if (i != 0) throw new ArgumentException("Đường đi tồn tại node không hợp lệ"); + + var angleForward = Math.Atan2(nodes[1].Y - nodes[0].Y, nodes[1].X - nodes[0].X); + var angleBackward = Math.Atan2(nodes[0].Y - nodes[1].Y, nodes[0].X - nodes[1].X); + OrderNodes.Add(new() + { + NodeId = nodes[i].Id.ToString(), + Released = true, + SequenceId = i, + NodeDescription = nodes[i].Name ?? "", + NodePosition = new NodePosition() + { + X = nodes[i].X, + Y = nodes[i].Y, + Theta = navigationType == NavigationType.OmniDrive ? 10 : nodes[i].Direction == Direction.FORWARD ? angleForward : angleBackward, + MapDescription = map.Name, + MapId = map.Id.ToString(), + AllowedDeviationTheta = nodes[i].AllowedDeviationTheta, + AllowedDeviationXY = nodes[i].AllowedDeviationXy, + }, + Actions = actions.TryGetValue(nodes[i].Name, out IEnumerable? anoCustomActions) ? [.. anoCustomActions] : [], + }); + continue; + } + + double angle; + if (i == 0) + { + var nearNode = MapEditorHelper.GetNearByNode(nodes[0], nodes[1], edges[0], 0.01); + var angleForward = Math.Atan2(nearNode.Y - nodes[0].Y, nearNode.X - nodes[0].X); + var angleBackward = Math.Atan2(nodes[0].Y - nearNode.Y, nodes[0].X - nearNode.X); + angle = nodes[i].Direction == Direction.FORWARD ? angleForward : angleBackward; + } + else + { + var nearNode = MapEditorHelper.GetNearByNode(nodes[i], nodes[i - 1], edges[i - 1], 0.01); + var angleForward = Math.Atan2(nodeDb.Y - nearNode.Y, nodeDb.X - nearNode.X); + var angleBackward = Math.Atan2(nearNode.Y - nodeDb.Y, nearNode.X - nodeDb.X); + + if (nodes[i].Direction == nodes[i - 1].Direction) angle = nodes[i].Direction == Direction.FORWARD ? angleForward : angleBackward; + else angle = nodes[i - 1].Direction == Direction.FORWARD ? angleForward : angleBackward; + } + + List nodeActions = ConvertAction(nodeDb.Actions, [.. map.Actions]); + if (actions.TryGetValue(nodes[i].Name, out IEnumerable? customActions) && customActions is not null) + { + nodeActions.AddRange(customActions); + } + OrderNodes.Add(new() + { + NodeId = nodes[i].Id.ToString(), + Released = i == 0 || !trafficEnable, + SequenceId = i, + NodeDescription = nodes[i].Name ?? "", + NodePosition = new NodePosition() + { + X = nodeDb.X, + Y = nodeDb.Y, + Theta = i == (nodes.Length - 1) ? (nodeDb.Theta * Math.PI / 180) : (navigationType == NavigationType.OmniDrive ? 10 : angle), + MapDescription = map.Name, + MapId = map.Id.ToString(), + AllowedDeviationTheta = nodeDb.AllowedDeviationTheta, + AllowedDeviationXY = nodeDb.AllowedDeviationXy, + }, + Actions = [.. nodeActions], + }); + } + foreach (var edge in edges) + { + var orderEdge = ConvertToOrderEdge(edge, map); + if (orderEdge is null) + { + if (edge == edges.First()) + { + orderEdge = new Edge() + { + EdgeId = edge.Id.ToString(), + StartNodeId = edge.StartNodeId.ToString(), + EndNodeId = edge.EndNodeId.ToString(), + EdgeDescription = $"{edge.StartNodeId} - {edge.EndNodeId}", + Direction = edge.DirectionAllowed.ToString(), + Orientation = 0, + Released = false, + Length = 0, + MaxRotationSpeed = 0.5, + OrientationType = "", + MaxSpeed = 0.5, + MinHeight = 0, + RotationAllowed = true, + Actions = [.. ConvertAction(edge.Actions, [.. map.Actions])], + Trajectory = new() + { + Degree = 1, + KnotVector = [0, 0, 1, 1], + ControlPoints = [new() + { + X = nodes[0].X, + Y = nodes[0].Y, + Weight = 1, + }, + new() + { + X = nodes[1].X, + Y = nodes[1].Y, + Weight = 1, + }], + }, + }; + } + else throw new ArgumentException("Đường đi tồn tại edge không hợp lệ"); + } + orderEdge.SequenceId = Array.IndexOf(edges, edge); + orderEdge.Released = !trafficEnable; + OrderEdges.Add(orderEdge); + } + orderMsgTemplate.Nodes = [.. OrderNodes]; + orderMsgTemplate.Edges = [.. OrderEdges]; + } + + private async Task> Order(List nodes, List edges, MapDataDto map, IDictionary>? actions, bool trafficManager) + { + try + { + var OrderMsg = new OrderMsg() + { + HeaderId = 1, + Manufacturer = Manufacturer, + Version = Version, + Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + SerialNumber = SerialNumber, + OrderId = Guid.NewGuid().ToString(), + OrderUpdateId = 1, + ZoneSetId = map.Name ?? "", + }; + CreateOrderMsg(ref OrderMsg, [.. nodes], [.. edges], map, actions ?? new Dictionary>(), NavigationType, trafficManager); + var pubOrder = await Order(OrderMsg); + if (pubOrder.IsSuccess) return new(true) { Data = OrderMsg }; + return new(false, pubOrder.Message); + } + catch (Exception ex) + { + string msg = "Có lỗi xảy ra trong quá trình tạo Order"; + Log($"{msg}: {ex.Message}", LogLevel.Warning); + return new(true, msg); + } + } + + private string GetState() + { + if (StateMsg.Information is not null) + { + var RobotGeneral = StateMsg.Information.FirstOrDefault(info => info.InfoType == InformationType.robot_general.ToString()); + if (RobotGeneral is not null) + { + var references = RobotGeneral.InfoReferences.FirstOrDefault(key => key.ReferenceKey == InformationReferencesKey.robot_state.ToString()); + if (references is not null) return references.ReferenceValue; + } + } + return ""; + } + + private RobotOrderDto GetOrderState() + { + return new() + { + OrderId = StateMsg.OrderId, + IsCompleted = (RobotOrder is not null && RobotOrder.IsCompleted) || RobotOrder is null, + IsError = RobotOrder is not null && RobotOrder.IsError, + IsProcessing = RobotOrder is not null && RobotOrder.IsProcessing, + IsCanceled = RobotOrder is not null && RobotOrder.IsCanceled, + Errors = RobotOrder is not null ? RobotOrder.Errors : [], + }; + } + + private RobotActionDto[] GetActionsState() + { + List Actions = []; + foreach (var action in StateMsg.ActionStates) + { + Actions.Add(new RobotActionDto() + { + ActionId = action.ActionId, + Action = action, + IsError = ActionFailed(action.ActionStatus), + IsCompleted = ActionFinish(action.ActionStatus), + IsProcessing = ActionRunning(action.ActionStatus), + Errors = [.. GetErrorString()], + }); + } + return [.. Actions]; + } + + private ErrorLevel GetErrorLevel() + { + if (StateMsg.Errors is not null) + { + if (StateMsg.Errors.Any(error => error.ErrorLevel == ErrorLevel.FATAL.ToString())) return ErrorLevel.FATAL; + if (StateMsg.Errors.Any(error => error.ErrorLevel == ErrorLevel.WARNING.ToString())) return ErrorLevel.WARNING; + } + return ErrorLevel.NONE; + } + + private static bool ActionFinish(string status) => status == ActionStatus.FINISHED.ToString(); + + private static bool ActionFailed(string status) => status == ActionStatus.FAILED.ToString(); + + private static bool ActionRunning(string status) => status == ActionStatus.RUNNING.ToString(); + + private IEnumerable GetErrorString() + { + if (StateMsg.Errors is not null && StateMsg.Errors.Length > 0) + { + foreach (var error in StateMsg.Errors) + { + yield return $"{error.ErrorType} - {error.ErrorDescription}"; + } + } + yield break; + } + + public Task MoveRandom(List nodes) + { + try + { + CancelRandom = new CancellationTokenSource(); + var random = new Random(); + var randomTask = Task.Run(async () => + { + while (!CancelRandom.IsCancellationRequested) + { + var index = random.Next(nodes.Count); + await MoveToNode(nodes[index]); + while (!CancelRandom.IsCancellationRequested) + { + if (!IsWorking) break; + await Task.Delay(1000); + } + await Task.Delay(2000); + } + }, cancellationToken: CancelRandom.Token); + } + catch { } + return Task.FromResult(new(true)); + } + + public void Dispose() + { + if (IsOnline) IsOnline = false; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.RobotManager/Services/Robot/RobotOrder.cs b/RobotNet.RobotManager/Services/Robot/RobotOrder.cs new file mode 100644 index 0000000..1c187b8 --- /dev/null +++ b/RobotNet.RobotManager/Services/Robot/RobotOrder.cs @@ -0,0 +1,593 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.RobotShares.VDA5050.Order; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.Shares; +using SixLabors.ImageSharp; +using Action = RobotNet.RobotShares.VDA5050.InstantAction.Action; + +namespace RobotNet.RobotManager.Services.Robot; + +public class RobotOrder : IRobotOrder, IDisposable +{ + public bool IsError { get; private set; } + public bool IsCompleted { get; private set; } + public bool IsProcessing => !IsDisposed; + public bool IsCanceled { get; private set; } + public string[] Errors => [.. GetError()]; + public Guid MapId { get; private set; } + public TrafficSolutionState TrafficSolutionState { get; private set; } = TrafficSolutionState.None; + public NavigationPathEdge[] FullPath => GetFullPath(); + public NavigationPathEdge[] BasePath => GetBasePath(); + public List CurrentZones { get; set; } = []; + + private const int intervalTime = 50; + private readonly WatchTimerAsync Timer; + private int TimerCounter = 0; + + private OrderMsg OrderMsg; + private readonly LoggerController Logger; + private readonly TrafficManager TrafficManager; + private readonly TrafficACS TrafficACS; + private readonly MapManager MapManager; + private readonly Func> PubOrder; + private readonly List Nodes; + private readonly List Edges; + private readonly Dictionary Zones; + private readonly IDictionary> Actions; + private readonly IRobotController RobotController; + private readonly NavigationType NavigationType; + private Guid TrafficManagerGoalId = Guid.Empty; + private Guid TrafficACSGoalId = Guid.Empty; + + private bool IsDisposed = false; + private bool IsWaittingCancel = false; + private Guid CurrentBaseId = Guid.Empty; + private Guid LastNodeId = Guid.Empty; + + public RobotOrder(NodeDto[] nodes, + EdgeDto[] edges, + Dictionary zones, + IDictionary> actions, + OrderMsg orderMsg, + IRobotController robotController, + NavigationType navigationType, + TrafficManager traffiManager, + TrafficACS trafficACS, + MapManager mapManager, + LoggerController logger, + Func> pubOrder) + { + if (nodes.Length < 2) + { + IsError = true; + IsDisposed = true; + throw new ArgumentException("Đường dẫn không hợp lệ. Số lượng nodes nhỏ hơn 2"); + } + if (nodes.Length < 1) + { + IsError = true; + IsDisposed = true; + throw new ArgumentException("Đường dẫn không hợp lệ. Số lượng edges nhỏ hơn 1"); + } + Nodes = [.. nodes]; + Edges = [.. edges]; + Zones = zones ?? []; + MapId = Nodes[1].MapId; + Actions = actions; + OrderMsg = orderMsg; + RobotController = robotController; + NavigationType = navigationType; + TrafficManager = traffiManager; + TrafficACS = trafficACS; + MapManager = mapManager; + Logger = logger; + PubOrder = pubOrder; + Timer = new(intervalTime, TimerHandler, logger); + Timer.Start(); + } + + private async Task PublishOrder(Guid goalId) + { + OrderMsg.OrderUpdateId++; + OrderMsg.HeaderId++; + OrderMsg.Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + var pubOrder = await PubOrder.Invoke(OrderMsg); + if (!pubOrder.IsSuccess) + { + OrderMsg.OrderUpdateId--; + OrderMsg.HeaderId--; + return false; + } + CurrentBaseId = goalId; + return true; + } + + private void UpdatePath(NodeDto[] nodes, EdgeDto[] edges) + { + try + { + var map = Task.Run(async () => await MapManager.GetMapData(MapId)); + map.Wait(); + if (map.Result is null) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order cập nhật tuyến đường mới có lỗi xảy ra: không thể lấy dữ liệu bản đồ {MapId}"); + else if (!map.Result.IsSuccess) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order cập nhật tuyến đường mới có lỗi xảy ra: {map.Result.Message}"); + else if (map.Result.Data is not null) + { + Robot.RobotController.CreateOrderMsg(ref OrderMsg, nodes, edges, map.Result.Data, Actions, NavigationType, TrafficManager.Enable); + Nodes.Clear(); + Nodes.AddRange(nodes); + Edges.Clear(); + Edges.AddRange(edges); + } + else Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order cập nhật tuyến đường mới có lỗi xảy ra: dữ liệu bản đồ {MapId} rỗng"); + } + catch (Exception ex) + { + Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order cập nhật tuyến đường mới có lỗi xảy ra: {ex}"); + } + } + + private bool UpdateGoal(Guid goalId) + { + var currentNodeOrderIndex = Array.FindIndex(OrderMsg.Nodes, n => n.NodeId == LastNodeId.ToString()); + if (currentNodeOrderIndex != -1) + { + OrderMsg.Nodes = [.. OrderMsg.Nodes.Skip(currentNodeOrderIndex)]; + OrderMsg.Edges = [.. OrderMsg.Edges.Skip(currentNodeOrderIndex)]; + } + + var goalOrder = OrderMsg.Nodes.FirstOrDefault(n => n.NodeId == goalId.ToString()); + if (goalOrder is not null && CurrentBaseId.ToString() != goalOrder.NodeId) + { + for (int i = 0; i <= Array.IndexOf(OrderMsg.Nodes, goalOrder); i++) + { + OrderMsg.Nodes[i].Released = true; + } + for (int i = 0; i < Array.IndexOf(OrderMsg.Nodes, goalOrder); i++) + { + OrderMsg.Edges[i].Released = true; + } + return true; + } + return false; + } + + private void HandleGiveWayState(TrafficSolution solution) + { + if (solution.GivewayNodes[^1].Id != solution.ReleaseNode.Id) return; + if (solution.GivewayEdges.Count == 0) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order xử lí tránh đường lỗi: Không có edge mới nhận được"); + else if (solution.GivewayNodes.Count <= 1) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order xử lí tránh đường lỗi: nodes mới nhận được có số lượng {solution.Nodes.Length}"); + else if (CurrentBaseId != solution.ReleaseNode.Id) + { + List nodes = [..solution.GivewayNodes.Select(n => new NodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Direction = MapCompute.GetNodeDirection(n.Direction), + })]; + List edges = [..solution.GivewayEdges.Select(e => new EdgeDto() + { + Id = e.Id, + ControlPoint1X = e.ControlPoint1X, + ControlPoint2X = e.ControlPoint2X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2Y = e.ControlPoint2Y, + TrajectoryDegree = e.TrajectoryDegree, + StartNodeId = e.StartNodeId, + EndNodeId = e.EndNodeId, + })]; + nodes.Add(nodes[^2]); + edges.Add(new() + { + Id = edges[^1].Id, + StartNodeId = nodes[^2].Id, + EndNodeId = nodes[^1].Id, + ControlPoint1X = edges[^1].ControlPoint2X, + ControlPoint1Y = edges[^1].ControlPoint2Y, + ControlPoint2X = edges[^1].ControlPoint1X, + ControlPoint2Y = edges[^1].ControlPoint1Y, + TrajectoryDegree = edges[^1].TrajectoryDegree, + }); + + UpdatePath([.. nodes], [.. edges]); + } + } + + private void HandleRefreshPath(TrafficSolution solution) + { + if (solution.Edges.Length == 0) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order xử lí làm mới đường lỗi: Không có edge mới nhận được"); + else if (solution.Nodes.Length <= 1) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order xử lí làm mới đường lỗi: nodes mới nhận được có số lượng {solution.Nodes.Length}"); + else if (solution.Nodes[^1].Id != Nodes[^1].Id) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order xử lí làm mới đường lỗi: tuyến đường mới không kết thúc tại đích cũ"); + else + { + List nodes = [..solution.Nodes.Select(n => new NodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Direction = MapCompute.GetNodeDirection(n.Direction), + })]; + List edges = [..solution.Edges.Select(e => new EdgeDto() + { + Id = e.Id, + StartNodeId = e.StartNodeId, + EndNodeId = e.EndNodeId, + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + TrajectoryDegree = e.TrajectoryDegree, + })]; + + UpdatePath([.. nodes], [.. edges]); + } + } + + private async Task RequestInACS(Dictionary newZones) + { + Guid trafficACSrelaseNodeId = Guid.Empty; + foreach (var zone in newZones) + { + if (zone.Value.Length == 0) trafficACSrelaseNodeId = zone.Key; + else + { + bool requestSuccess = true; + foreach (var zoneACS in zone.Value) + { + if (string.IsNullOrEmpty(zoneACS.Name)) continue; + if (CurrentZones.Any(z => z.Id == zoneACS.Id)) continue; + + var getTrafficACS = await TrafficACS.RequestIn(RobotController.SerialNumber, zoneACS.Name); + if (getTrafficACS.IsSuccess && getTrafficACS.Data) CurrentZones.Add(zoneACS); + else + { + requestSuccess = false; + break; + } + } + if (requestSuccess) trafficACSrelaseNodeId = zone.Key; + else break; + } + } + return trafficACSrelaseNodeId; + } + + private async Task UpdateTraffic() + { + var trafficManagerGoalIndex = Nodes.FindIndex(n => n.Id == TrafficManagerGoalId); + var trafficACSGoalIndex = Nodes.FindIndex(n => n.Id == TrafficACSGoalId); + if (trafficManagerGoalIndex != -1 && trafficACSGoalIndex != -1) + { + var goalIndex = Math.Min(trafficManagerGoalIndex, trafficACSGoalIndex); + NodeDto goal = Nodes[goalIndex]; + var updatemsg = UpdateGoal(Nodes[goalIndex].Id); + if (updatemsg && Nodes[goalIndex].Id != CurrentBaseId) + { + var publish = await PublishOrder(Nodes[goalIndex].Id); + if (!publish) Logger.Warning($"{RobotController.StateMsg.SerialNumber} - Robot Order publish xảy ra lỗi"); + } + } + } + + private async Task GoOutTrafficACS(Guid lastNodeId) + { + if (CurrentBaseId == Guid.Empty) return; + var goalIndex = Nodes.FindIndex(n => n.Id == CurrentBaseId); + if (goalIndex == -1) return; + var inNodeIndex = Nodes.FindIndex(n => n.Id == lastNodeId); + inNodeIndex = inNodeIndex != -1 ? inNodeIndex : 0; + if (goalIndex <= inNodeIndex) return; + var baseNodes = Nodes.GetRange(inNodeIndex, goalIndex + 1 - inNodeIndex); + var baseZones = PathPlanner.GetZones([.. baseNodes], Zones); + var outZones = CurrentZones.Where(z => !baseZones.Any(bz => bz.Id == z.Id)).ToList(); + foreach (var zoneACS in outZones) + { + if (string.IsNullOrEmpty(zoneACS.Name)) continue; + var outTrafficACS = await TrafficACS.RequestOut(RobotController.SerialNumber, zoneACS.Name); + if (outTrafficACS.IsSuccess && outTrafficACS.Data) CurrentZones.RemoveAll(z => z.Id == zoneACS.Id); + } + } + + private Guid GetLastNodeId() + { + Guid lastNodeId = Guid.Empty; + double minDeviationDistance = TrafficACS.DeviationDistance; + foreach (var node in Nodes) + { + var distance = Math.Sqrt(Math.Pow(RobotController.VisualizationMsg.AgvPosition.X - node.X, 2) + Math.Pow(RobotController.VisualizationMsg.AgvPosition.Y - node.Y, 2)); + if (distance < minDeviationDistance) + { + minDeviationDistance = distance; + lastNodeId = node.Id; + } + } + return lastNodeId; + } + + private async Task TimerHandler() + { + try + { + var lastNodeId = GetLastNodeId(); + if (lastNodeId != Guid.Empty && LastNodeId != lastNodeId) LastNodeId = lastNodeId; + + if (TimerCounter++ > 10) + { + TimerCounter = 0; + if (IsOrderClear()) + { + if (RobotController.StateMsg.LastNodeId == Nodes[^1].Id.ToString() && GetActionFinished()) + { + IsCompleted = true; + Logger.Info($"{RobotController.StateMsg.SerialNumber} - Robot Order đã hoàn thành tới đích {Nodes[^1].Name}"); + } + else if (RobotController.StateMsg.LastNodeId != Nodes[^1].Id.ToString() || GetActionFailed()) + { + Logger.Error($"{RobotController.StateMsg.SerialNumber} - Robot Order kết thúc lỗi. {string.Join(", ", GetError())}"); + IsError = true; + } + } + if (!RobotController.IsOnline) + { + Logger.Error($"{RobotController.StateMsg.SerialNumber} - Robot Order kết thúc: robot mất kết nối"); + IsError = true; + } + if (IsCompleted || IsError || (IsWaittingCancel && IsOrderClear())) + { + Dispose(); + return; + } + if (!IsWaittingCancel) + { + await GoOutTrafficACS(LastNodeId); + + if (!TrafficManager.Enable) + { + if (TrafficManagerGoalId != Nodes[^1].Id) TrafficManagerGoalId = Nodes[^1].Id; + } + else + { + TrafficManager.UpdateInNode(RobotController.SerialNumber, LastNodeId); + var trafficSolution = TrafficManager.GetTrafficNode(RobotController.SerialNumber); + switch (trafficSolution.State) + { + case TrafficSolutionState.GiveWay: + HandleGiveWayState(trafficSolution); + break; + case TrafficSolutionState.RefreshPath: + HandleRefreshPath(trafficSolution); + break; + case TrafficSolutionState.Complete: + case TrafficSolutionState.Waitting: + default: + break; + } + TrafficSolutionState = trafficSolution.State; + var goal = Nodes.FirstOrDefault(n => n.Id == trafficSolution.ReleaseNode.Id); + if (goal is not null && goal.Id != TrafficManagerGoalId) TrafficManagerGoalId = goal.Id; + } + + if (!TrafficACS.Enable) + { + if (TrafficACSGoalId != Nodes[^1].Id) TrafficACSGoalId = Nodes[^1].Id; + } + else + { + var newZones = TrafficACS.GetZones(LastNodeId, [.. Nodes], Zones); + var trafficACSrelaseNodeId = await RequestInACS(newZones); + if (trafficACSrelaseNodeId != Guid.Empty && trafficACSrelaseNodeId != TrafficACSGoalId) TrafficACSGoalId = trafficACSrelaseNodeId; + } + + await UpdateTraffic(); + } + } + } + catch (Exception ex) + { + Logger.Error($"{RobotController.StateMsg.SerialNumber} - Robot Order Handler xảy ra lỗi: {ex}"); + } + } + + private IEnumerable GetError() + { + if (RobotController.StateMsg.Errors is not null && RobotController.StateMsg.Errors.Length > 0) + { + foreach (var error in RobotController.StateMsg.Errors) + { + yield return $"{error.ErrorType} - {error.ErrorDescription}"; + } + } + yield break; + } + + private bool GetActionFinished() + { + if (RobotController.StateMsg.ActionStates is not null && RobotController.StateMsg.ActionStates.Length > 0) + { + var finalActions = OrderMsg.Nodes[^1].Actions; + foreach (var actionState in RobotController.StateMsg.ActionStates) + { + if (finalActions.Any(a => a.ActionId == actionState.ActionId) && actionState.ActionStatus != ActionStatus.FINISHED.ToString()) return false; + } + } + return true; + } + + private bool GetActionFailed() + { + if (RobotController.StateMsg.ActionStates is not null && RobotController.StateMsg.ActionStates.Length > 0) + { + var finalActions = OrderMsg.Nodes[^1].Actions; + foreach (var actionState in RobotController.StateMsg.ActionStates) + { + if (finalActions.Any(a => a.ActionId == actionState.ActionId) && actionState.ActionStatus == ActionStatus.FAILED.ToString()) return true; + } + } + return false; + } + + public async Task Cancel(CancellationToken cancellation) + { + try + { + IsWaittingCancel = true; + while (!cancellation.IsCancellationRequested) + { + if (IsDisposed) + { + IsCanceled = true; + return true; + } + if (Timer.Disposed) Dispose(); + try { await Task.Delay(500, cancellation); } catch { } + } + IsWaittingCancel = false; + return false; + } + catch + { + IsWaittingCancel = false; + return false; + } + } + + private NavigationPathEdge[] SplitChecking(NodeDto lastNode, NodeDto nearLastNode, EdgeDto edge) + { + List pathEdges = []; + var splitStartPath = PathPlanner.PathSplit([new NodeDto + { + Id = lastNode.Id, + X = lastNode.X, + Y = lastNode.Y, + }, new NodeDto + { + Id = nearLastNode.Id, + X = nearLastNode.X, + Y = nearLastNode.Y, + }], [edge], 0.1); + if (splitStartPath.IsSuccess && splitStartPath.Data is not null) + { + int index = 0; + double minDistance = double.MaxValue; + for (int i = 0; i < splitStartPath.Data.Length; i++) + { + var distance = Math.Sqrt(Math.Pow(RobotController.VisualizationMsg.AgvPosition.X - splitStartPath.Data[i].X, 2) + + Math.Pow(RobotController.VisualizationMsg.AgvPosition.Y - splitStartPath.Data[i].Y, 2)); + if (distance < minDistance) + { + minDistance = distance; + index = i; + } + } + for (int i = index; i < splitStartPath.Data.Length - 1; i++) + { + pathEdges.Add(new() + { + StartX = splitStartPath.Data[i].X, + StartY = splitStartPath.Data[i].Y, + EndX = splitStartPath.Data[i + 1].X, + EndY = splitStartPath.Data[i + 1].Y, + Degree = 1, + }); + } + } + return [.. pathEdges]; + } + + private NavigationPathEdge[] GetFullPath() + { + List pathEdges = []; + if (RobotController.StateMsg.NodeStates is not null && RobotController.StateMsg.NodeStates.Length > 0) + { + var lastNodeIndex = Nodes.FindIndex(n => n.Id == LastNodeId); + lastNodeIndex = lastNodeIndex != -1 ? lastNodeIndex : 0; + if (lastNodeIndex < Nodes.Count - 1) pathEdges = [.. SplitChecking(Nodes[lastNodeIndex], Nodes[lastNodeIndex + 1], Edges[lastNodeIndex])]; + if (lastNodeIndex < Nodes.Count - 2) + { + var nodes = Nodes.GetRange(lastNodeIndex + 1, Nodes.Count - lastNodeIndex - 1); + var edges = Edges.GetRange(lastNodeIndex + 1, nodes.Count - 1); + for (int i = 0; i < nodes.Count - 1; i++) + { + pathEdges.Add(new() + { + StartX = nodes[i].X, + StartY = nodes[i].Y, + EndX = nodes[i + 1].X, + EndY = nodes[i + 1].Y, + ControlPoint1X = edges[i].ControlPoint1X, + ControlPoint1Y = edges[i].ControlPoint1Y, + ControlPoint2X = edges[i].ControlPoint2X, + ControlPoint2Y = edges[i].ControlPoint2Y, + Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, + }); + } + } + } + return [.. pathEdges]; + } + + private NavigationPathEdge[] GetBasePath() + { + List pathEdges = []; + if (RobotController.StateMsg.NodeStates is not null && RobotController.StateMsg.NodeStates.Length > 0) + { + var lastNodeIndex = Nodes.FindIndex(n => n.Id == LastNodeId); + lastNodeIndex = lastNodeIndex != -1 ? lastNodeIndex : 0; + var baseNodeIndex = Nodes.FindIndex(n => n.Id == CurrentBaseId); + baseNodeIndex = baseNodeIndex != -1 ? baseNodeIndex : Nodes.Count - 1; + + if (lastNodeIndex < baseNodeIndex) + { + if (lastNodeIndex < Nodes.Count - 1) pathEdges = [.. SplitChecking(Nodes[lastNodeIndex], Nodes[lastNodeIndex + 1], Edges[lastNodeIndex])]; + if (lastNodeIndex < Nodes.Count - 2 && lastNodeIndex < baseNodeIndex - 1) + { + var nodes = Nodes.GetRange(lastNodeIndex + 1, baseNodeIndex - lastNodeIndex); + if (nodes.Count > 1) + { + var edges = Edges.GetRange(lastNodeIndex + 1, nodes.Count - 1); + for (int i = 0; i < nodes.Count - 1; i++) + { + pathEdges.Add(new() + { + StartX = nodes[i].X, + StartY = nodes[i].Y, + EndX = nodes[i + 1].X, + EndY = nodes[i + 1].Y, + ControlPoint1X = edges[i].ControlPoint1X, + ControlPoint1Y = edges[i].ControlPoint1Y, + ControlPoint2X = edges[i].ControlPoint2X, + ControlPoint2Y = edges[i].ControlPoint2Y, + Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, + }); + } + } + } + } + } + return [.. pathEdges]; + } + + private bool IsOrderClear() => RobotController.StateMsg.NodeStates.Length == 0 && RobotController.StateMsg.EdgeStates.Length == 0; + + public void CreateComledted() + { + IsCompleted = true; + IsCanceled = false; + IsError = false; + } + + public void Dispose() + { + IsDisposed = true; + Timer.Dispose(); + TrafficManager.DeleteAgent(RobotController.SerialNumber); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.RobotManager/Services/RobotEditorStorageRepository.cs b/RobotNet.RobotManager/Services/RobotEditorStorageRepository.cs new file mode 100644 index 0000000..8d36438 --- /dev/null +++ b/RobotNet.RobotManager/Services/RobotEditorStorageRepository.cs @@ -0,0 +1,129 @@ +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; + +namespace RobotNet.RobotManager.Services; + +public class RobotEditorStorageRepository +{ + private class MinioOption + { + public bool UsingLocal { get; set; } + public string? Endpoint { get; set; } + public string? User { get; set; } + public string? Password { get; set; } + public string? Bucket { get; set; } + } + private readonly IMinioClient MinioClient; + private readonly MinioOption MinioOptions = new(); + public RobotEditorStorageRepository(IConfiguration configuration) + { + configuration.Bind("MinIO", MinioOptions); + MinioClient = new MinioClient() + .WithEndpoint(MinioOptions.Endpoint) + .WithCredentials(MinioOptions.User, MinioOptions.Password) + .WithSSL(false) + .Build(); + } + + + public async Task<(bool, string)> UploadAsync(string path, string objectName, Stream data, long size, string contentType, CancellationToken cancellationToken) + { + try + { + if (!MinioOptions.UsingLocal) + { + var beArgs = new BucketExistsArgs().WithBucket(MinioOptions.Bucket); + bool found = await MinioClient.BucketExistsAsync(beArgs, cancellationToken).ConfigureAwait(false); + if (!found) + { + var mbArgs = new MakeBucketArgs() + .WithBucket(MinioOptions.Bucket); + await MinioClient.MakeBucketAsync(mbArgs, cancellationToken).ConfigureAwait(false); + } + var getListBucketsTask = await MinioClient.ListBucketsAsync(cancellationToken); + var putObjectArgs = new PutObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}") + .WithObjectSize(size) + .WithStreamData(data) + .WithContentType(contentType); + await MinioClient.PutObjectAsync(putObjectArgs, cancellationToken).ConfigureAwait(false); + return (true, ""); + } + + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (!Directory.Exists(mapImageFolder)) Directory.CreateDirectory(mapImageFolder); + var pathLocal = Path.Combine(mapImageFolder, $"{objectName}.png"); + if (File.Exists($"{pathLocal}.bk")) File.Delete($"{pathLocal}.bk"); + if (File.Exists(pathLocal)) File.Move(pathLocal, $"{pathLocal}.bk"); + using (Stream fileStream = new FileStream(pathLocal, FileMode.Create)) + { + await data.CopyToAsync(fileStream, cancellationToken); + } + return (true, ""); + } + catch (MinioException ex) + { + return (false, ex.Message); + } + } + + public (bool usingLocal, string url) GetUrl(string path, string objectName) + { + try + { + if (!MinioOptions.UsingLocal) + { + var presignedGetObjectArgs = new PresignedGetObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}") + .WithExpiry(60 * 60 * 24); + var url = MinioClient.PresignedGetObjectAsync(presignedGetObjectArgs); + url.Wait(); + return (false, url.Result); + } + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (Directory.Exists(mapImageFolder)) + { + var pathLocal = Path.Combine(mapImageFolder, $"{objectName}.png"); + if (File.Exists(pathLocal)) + { + return (true, pathLocal); + } + } + return (true, ""); + } + catch (MinioException e) + { + return (false, e.Message); + } + } + + public async Task<(bool, string)> DeleteAsync(string path, string objectName, CancellationToken cancellationToken) + { + try + { + if (!MinioOptions.UsingLocal) + { + var removeObjectArgs = new RemoveObjectArgs() + .WithBucket(MinioOptions.Bucket) + .WithObject($"{path}/{objectName}"); + await MinioClient.RemoveObjectAsync(removeObjectArgs, cancellationToken).ConfigureAwait(false); + return (true, ""); + } + var mapImageFolder = string.IsNullOrEmpty(MinioOptions.Bucket) ? "MapImages" : MinioOptions.Bucket; + if (Directory.Exists(mapImageFolder)) + { + var pathLocal = Path.Combine(mapImageFolder, $"{objectName}.png"); + if (File.Exists(pathLocal)) File.Delete(pathLocal); + if (File.Exists($"{pathLocal}.bk")) File.Delete($"{pathLocal}.bk"); + } + return (true, ""); + } + catch (MinioException ex) + { + return (false, ex.Message); + } + } +} diff --git a/RobotNet.RobotManager/Services/RobotManager.cs b/RobotNet.RobotManager/Services/RobotManager.cs new file mode 100644 index 0000000..c6158ec --- /dev/null +++ b/RobotNet.RobotManager/Services/RobotManager.cs @@ -0,0 +1,562 @@ +using MQTTnet; +using MQTTnet.Protocol; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services.Robot; +using RobotNet.RobotManager.Services.Simulation; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.OpenACS; +using RobotNet.RobotShares.VDA5050; +using RobotNet.RobotShares.VDA5050.Connection; +using RobotNet.RobotShares.VDA5050.Factsheet; +using RobotNet.RobotShares.VDA5050.FactsheetExtend; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; +using RobotNet.Script.Expressions; +using RobotNet.Shares; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json; + +namespace RobotNet.RobotManager.Services; + +public class RobotManager : BackgroundService +{ + public IEnumerable RobotSerialNumbers => RobotControllers.Keys; + + private readonly VDA5050Setting VDA5050Setting = new(); + private Dictionary RobotControllers { get; } = []; + private readonly IServiceProvider ServiceProvider; + private IMqttClient? MQTTClient; + private readonly LoggerController Logger; + + public RobotManager(IConfiguration configuration, IServiceProvider serviceProvider, LoggerController logger) + { + configuration.Bind("VDA5050Setting", VDA5050Setting); + ServiceProvider = serviceProvider; + Logger = logger; + } + + public IRobotController? this[string robotid] => RobotControllers.TryGetValue(robotid, out IRobotController? value) ? value : null; + + private RobotController? AddRobotController(string robotId) + { + using var scope = ServiceProvider.CreateScope(); + var ApplicationDb = scope.ServiceProvider.GetRequiredService(); + + var robotdb = ApplicationDb.Robots.FirstOrDefault(robot => robot.RobotId == robotId); + if (robotdb is null) return null; + var robotModel = ApplicationDb.RobotModels.FirstOrDefault(model => model.Id == robotdb.ModelId); + if (robotModel is null) return null; + + var robotController = new RobotController(robotId, VDA5050Setting.Manufacturer, VDA5050Setting.Version, robotModel.NavigationType, ServiceProvider, PublishMQTT); + RobotControllers.Add(robotId, robotController); + return robotController; + } + + public void AddRobotSimulation(IEnumerable robotIds) + { + using var scope = ServiceProvider.CreateScope(); + var ApplicationDb = scope.ServiceProvider.GetRequiredService(); + var Traffic = scope.ServiceProvider.GetRequiredService(); + + foreach (var robotId in robotIds) + { + if (RobotControllers.TryGetValue(robotId, out _)) continue; + var robotdb = ApplicationDb.Robots.FirstOrDefault(robot => robot.RobotId == robotId); + if (robotdb is null) return; + var robotModel = ApplicationDb.RobotModels.FirstOrDefault(model => model.Id == robotdb.ModelId); + if (robotModel is null) return; + + var robotController = new RobotSimulation(robotId, robotModel, ServiceProvider); + RobotControllers.Add(robotId, robotController); + } + } + + public bool DeleteRobot(string robotId) + { + try + { + if (RobotControllers.TryGetValue(robotId, out var robotcontroller)) + { + RobotControllers.Remove(robotId); + } + return true; + } + catch (Exception ex) + { + Logger.Warning($"Hệ thống xảy ra lỗi khi xóa bỏ robot: {ex.Message}"); + return false; + } + } + + public IEnumerable GetRobotInfo(Guid mapId) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var RobotDb = scope.ServiceProvider.GetRequiredService(); + List RobotInfos = []; + foreach (var robotcontroller in RobotControllers.Values.ToList()) + { + var robotDb = RobotDb.Robots.FirstOrDefault(r => robotcontroller.SerialNumber == r.RobotId); + if (robotDb is null || robotDb.MapId != mapId) continue; + RobotInfos.Add(new() + { + RobotId = robotcontroller.SerialNumber, + Name = robotDb?.Name, + MapId = robotDb is null ? Guid.Empty : robotDb.MapId, + Battery = new() + { + BatteryHealth = robotcontroller.StateMsg.BatteryState.BatteryHealth, + BatteryCharge = robotcontroller.StateMsg.BatteryState.BatteryCharge, + BatteryVoltage = robotcontroller.StateMsg.BatteryState.BatteryVoltage, + Charging = robotcontroller.StateMsg.BatteryState.Charging, + }, + Errors = [], + Infomations = [], + Navigation = new() + { + NavigationState = robotcontroller.State, + RobotPath = robotcontroller.FullPath, + RobotBasePath = robotcontroller.BasePath, + }, + AgvPosition = new() + { + X = robotcontroller.VisualizationMsg.AgvPosition.X, + Y = robotcontroller.VisualizationMsg.AgvPosition.Y, + Theta = robotcontroller.VisualizationMsg.AgvPosition.Theta, + PositionInitialized = robotcontroller.VisualizationMsg.AgvPosition.PositionInitialized, + LocalizationScore = robotcontroller.VisualizationMsg.AgvPosition.LocalizationScore, + DeviationRange = robotcontroller.VisualizationMsg.AgvPosition.DeviationRange, + }, + AgvVelocity = new() + { + Vx = robotcontroller.VisualizationMsg.Velocity.Vx, + Vy = robotcontroller.VisualizationMsg.Velocity.Vy, + Omega = robotcontroller.VisualizationMsg.Velocity.Omega, + }, + Loads = robotcontroller.StateMsg.Loads, + }); + + } + return RobotInfos; + } + catch (Exception ex) + { + Logger.Warning($"Get Robot Info Error: - {ex}"); + return []; + } + } + + public RobotACSLockedDto[] GetRobotACSLocked() + { + try + { + List robotACSLockedDtos = []; + foreach(var robot in RobotControllers.Values) + { + robotACSLockedDtos.Add(new() + { + RobotId = robot.SerialNumber, + ZoneIds = robot.CurrentZones, + }); + } + return [..robotACSLockedDtos]; + } + catch (Exception ex) + { + Logger.Warning($"Get Robot ACS Locked Error: - {ex}"); + return []; + } + } + + private static Script.Expressions.RobotState ConvertIRobotControllerToRobotState(IRobotController robotController) + { + bool isReady = robotController.IsOnline && !robotController.OrderState.IsProcessing && robotController.StateMsg.Errors.Length == 0; + if (robotController.ActionStates.Length > 0) + { + isReady = isReady && robotController.ActionStates.All(a => !a.IsProcessing); + } + return new RobotState(isReady, + robotController.StateMsg.BatteryState.BatteryVoltage, + robotController.StateMsg.Loads.Length != 0, + robotController.StateMsg.BatteryState.Charging, + robotController.StateMsg.AgvPosition.X, + robotController.StateMsg.AgvPosition.Y, + robotController.StateMsg.AgvPosition.Theta); + } + + /// + /// Tìm kiếm robot theo tên model, tên bản đồ + /// + /// + /// + /// + public async Task> SearchRobot(string? robotModelName, string? mapName, Func funcSearch) + { + List robotIds = []; + try + { + using var scope = ServiceProvider.CreateScope(); + var RobotDbContext = scope.ServiceProvider.GetRequiredService(); + var MapManager = scope.ServiceProvider.GetRequiredService(); + var robotModel = RobotDbContext.RobotModels.FirstOrDefault(m => m.ModelName == robotModelName); + if (robotModel is null) return new(false, $"Robot model name {robotModelName} không tồn tại"); + var map = await MapManager.GetMapInfo(mapName ?? ""); + if (!map.IsSuccess && !string.IsNullOrEmpty(mapName)) return new(false, map.Message); + if (map is null || map.Data is null) return new(false, $"Map name {mapName} không tồn tại"); + + foreach (var robot in RobotControllers.Values) + { + if (!robot.IsOnline) continue; + var robotDb = RobotDbContext.Robots.FirstOrDefault(r => r.RobotId == robot.SerialNumber); + if (robotDb is null) continue; + var robotDbModel = RobotDbContext.RobotModels.FirstOrDefault(m => m.Id == robotDb.ModelId); + if (robotDbModel is null) continue; + var robotDbMap = await MapManager.GetMapInfo(robotDb.MapId); + if (robotDbMap is null || !robotDbMap.IsSuccess) continue; + if (robotDbMap.Data is null) continue; + + bool isRobotModelMatch = string.IsNullOrEmpty(robotModelName) || robotDbModel.Id == robotModel.Id; + bool isMapMatch = string.IsNullOrEmpty(mapName) || (map is not null && robotDbMap.Data.Id == map.Data.Id); + bool isExpressionMatch = funcSearch(ConvertIRobotControllerToRobotState(robot)); + + if (isRobotModelMatch && isMapMatch && isExpressionMatch) robotIds.Add(robot.SerialNumber); + } + return new(true, robotIds.Count > 0 ? "Tìm thấy robot" : "Không tìm thấy robot nào thỏa mãn điều kiện") + { + Data = [.. robotIds], + }; + } + catch (Exception ex) + { + Logger.Warning($"Find Robot Error: {ex}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } + + public RobotVDA5050StateDto? GetRobotVDA5050State(string robotId) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var robotDb = scope.ServiceProvider.GetRequiredService(); + if (RobotControllers.TryGetValue(robotId, out IRobotController? robotController) && robotController is not null) + { + var robot = robotDb.Robots.FirstOrDefault(r => r.RobotId == robotId); + if (robot is null) return null; + + return new() + { + RobotId = robotId, + Name = robot.Name, + MapId = robot.MapId, + Online = robotController.IsOnline, + State = robotController.StateMsg, + OrderState = robotController.OrderState, + IsWorking = robotController.IsWorking, + Visualization = robotController.VisualizationMsg, + }; + } + return null; + } + catch (Exception ex) + { + Logger.Warning($"Get VDA State Error: {robotId} - {ex}"); + return null; + } + } + + public IEnumerable GetRobotOnlineState() + { + try + { + using var scope = ServiceProvider.CreateScope(); + var robotDb = scope.ServiceProvider.GetRequiredService(); + var robots = robotDb.Robots.ToList(); + return [.. robots.Select(robot => + { + var robotOnline = RobotControllers.TryGetValue(robot.RobotId, out IRobotController? robotController); + return new RobotOnlineStateDto() + { + RobotId = robot.RobotId, + State = !robotOnline || robotController is null ? "OFFLINE" : robotController.State, + IsOnline = robotOnline && robotController is not null && robotController.IsOnline, + Battery = !robotOnline || robotController is null ? 0 : robotController.StateMsg.BatteryState.BatteryHealth, + }; + })]; + } + catch (Exception ex) + { + Logger.Warning($"Get Robot Online State Error: {ex}"); + return []; + } + } + + private bool PublishMQTT(string topic, string data) + { + var repeat = VDA5050Setting.Repeat; + while (repeat-- > 0) + { + try + { + var applicationMessage = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(data) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .Build(); + if (MQTTClient is null) return false; + var publish = MQTTClient.PublishAsync(applicationMessage, CancellationToken.None); + publish.Wait(); + if (!publish.Result.IsSuccess) continue; + return publish.Result.IsSuccess; + } + catch + { + return false; + } + } + return false; + } + + private void ConnectionChanged(string data) + { + try + { + var msg = JsonSerializer.Deserialize(data); + if (msg is null || string.IsNullOrEmpty(msg.SerialNumber)) return; + IRobotController robotController; + if (RobotControllers.TryGetValue(msg.SerialNumber, out IRobotController? value)) + { + value.IsOnline = msg.ConnectionState == ConnectionState.ONLINE.ToString(); + robotController = value; + } + else + { + var addRobtoControllerTask = AddRobotController(msg.SerialNumber); + if (addRobtoControllerTask is null) return; + robotController = addRobtoControllerTask; + RobotControllers[msg.SerialNumber].IsOnline = msg.ConnectionState == ConnectionState.ONLINE.ToString(); + } + robotController.RobotUpdated.Set(); + } + catch (Exception ex) + { + Logger.Warning($"Robot Manager ConnectionChanged xảy ra lỗi: {ex.Message} - {ex.StackTrace}"); + } + } + + private void StateChanged(string data) + { + try + { + var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Write); + if (msg is null || string.IsNullOrEmpty(msg.SerialNumber)) return; + + if (msg.AgvPosition is null) return; + msg.AgvPosition.Theta = msg.AgvPosition.Theta * 180 / Math.PI; + IRobotController robotController; + if (RobotControllers.TryGetValue(msg.SerialNumber, out IRobotController? value)) + { + value.StateMsg = msg; + robotController = value; + } + else + { + var addRobtoControllerTask = AddRobotController(msg.SerialNumber); + if (addRobtoControllerTask is null) return; + robotController = addRobtoControllerTask; + RobotControllers[msg.SerialNumber].StateMsg = msg; + } + robotController.IsOnline = true; + robotController.RobotUpdated.Set(); + } + catch (Exception ex) + { + Logger.Warning($"Robot Manager StateChanged xảy ra lỗi: {ex.Message} - {ex.StackTrace}"); + } + } + + private void VisualizationChanged(string data) + { + try + { + var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Write); + if (msg is null || string.IsNullOrEmpty(msg.SerialNumber)) return; + if (msg.AgvPosition is null) return; + msg.AgvPosition.Theta = msg.AgvPosition.Theta * 180 / Math.PI; + IRobotController robotController; + if (RobotControllers.TryGetValue(msg.SerialNumber, out IRobotController? value)) + { + value.VisualizationMsg = msg; + robotController = value; + } + else + { + var addRobtoControllerTask = AddRobotController(msg.SerialNumber); + if (addRobtoControllerTask is null) return; + robotController = addRobtoControllerTask; + RobotControllers[msg.SerialNumber].VisualizationMsg = msg; + } + robotController.IsOnline = true; + robotController.RobotUpdated.Set(); + } + catch (Exception ex) + { + Logger.Warning($"Robot Manager VisualizationChanged xảy ra lỗi: {ex.Message} - {ex.StackTrace}"); + } + } + + private void FactsheetChanged(string data) + { + try + { + var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Write); + if (msg is null || string.IsNullOrEmpty(msg.SerialNumber)) return; + IRobotController robotController; + if (RobotControllers.TryGetValue(msg.SerialNumber, out IRobotController? value)) + { + value.FactSheetMsg = msg; + robotController = value; + } + else + { + var addRobtoControllerTask = AddRobotController(msg.SerialNumber); + if (addRobtoControllerTask is null) return; + robotController = addRobtoControllerTask; + RobotControllers[msg.SerialNumber].FactSheetMsg = msg; + } + robotController.IsOnline = true; + robotController.RobotUpdated.Set(); + } + catch (Exception ex) + { + Logger.Warning($"Robot Manager FactsheetChanged xảy ra lỗi: {ex.Message} - {ex.StackTrace}"); + } + } + + private void FactsheetExtendChanged(string data) + { + try + { + var msg = JsonSerializer.Deserialize(data, JsonOptionExtends.Write); + if (msg is null || string.IsNullOrEmpty(msg.SerialNumber)) return; + IRobotController robotController; + if (RobotControllers.TryGetValue(msg.SerialNumber, out IRobotController? value)) + { + value.FactsheetExtendMsg = msg; + robotController = value; + } + else + { + var addRobtoControllerTask = AddRobotController(msg.SerialNumber); + if (addRobtoControllerTask is null) return; + robotController = addRobtoControllerTask; + RobotControllers[msg.SerialNumber].FactsheetExtendMsg = msg; + } + robotController.IsOnline = true; + robotController.RobotUpdated.Set(); + } + catch (Exception ex) + { + Logger.Warning($"Robot Manager FactsheetExtendChanged xảy ra lỗi: {ex.Message} - {ex.StackTrace}"); + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + while (!stoppingToken.IsCancellationRequested) + { + try + { + var mqttFactory = new MqttClientFactory(); + + MQTTClient = mqttFactory.CreateMqttClient(); + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer(VDA5050Setting.HostServer, VDA5050Setting.Port) + .WithClientId("FleetManager") + .WithCredentials(VDA5050Setting.UserName, VDA5050Setting.Password) + .Build(); + + if ((await MQTTClient.ConnectAsync(mqttClientOptions, stoppingToken)).ResultCode != MqttClientConnectResultCode.Success) + { + await Task.Delay(1000, stoppingToken); + continue; + } + + Logger.Info("Fleet Manager kết nối tới broker thành công"); + var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder() + .WithTopicFilter(f => + { + f.WithTopic(VDA5050Topic.Connection); + }) + .WithTopicFilter(f => + { + f.WithTopic(VDA5050Topic.Visualization); + }) + .WithTopicFilter(f => + { + f.WithTopic(VDA5050Topic.State); + }) + .WithTopicFilter(f => + { + f.WithTopic(VDA5050Topic.Factsheet); + }) + .WithTopicFilter(f => + { + f.WithTopic(VDA5050Topic.FactsheetExtend); + }) + .Build(); + + var response = await MQTTClient.SubscribeAsync(mqttSubscribeOptions, stoppingToken); + Logger.Info("Fleet Manager Subscribe thành công"); + MQTTClient.ApplicationMessageReceivedAsync += delegate (MqttApplicationMessageReceivedEventArgs args) + { + var stringData = Encoding.Default.GetString(args.ApplicationMessage.Payload); + switch (args.ApplicationMessage.Topic) + { + case VDA5050Topic.Connection: ConnectionChanged(stringData); break; + case VDA5050Topic.Visualization: VisualizationChanged(stringData); break; + case VDA5050Topic.State: StateChanged(stringData); break; + case VDA5050Topic.Factsheet: FactsheetChanged(stringData); break; + case VDA5050Topic.FactsheetExtend: FactsheetExtendChanged(stringData); break; + default: break; + } + return Task.CompletedTask; + }; + Logger.Info("Fleet Manager Subscribe event thành công"); + break; + } + catch (Exception ex) + { + Logger.Warning($"Kết nối tới broker xảy ra lỗi: {ex.Message}"); + await Task.Delay(3000, stoppingToken); + } + } + + Logger.Info("Fleet Manager bắt đầu kiểm tra kết nối tới robot"); + while (!stoppingToken.IsCancellationRequested) + { + try + { + foreach (var robotController in RobotControllers) + { + if (!robotController.Value.RobotUpdated.WaitOne(TimeSpan.FromSeconds(1)) && robotController.Value.IsOnline) + { + robotController.Value.Dispose(); + continue; + } + robotController.Value.RobotUpdated.Reset(); + } + await Task.Delay(TimeSpan.FromSeconds(VDA5050Setting.CheckingRobotMsgTimout), stoppingToken); + } + catch (Exception ex) + { + Logger.Warning($"Kiểm tra kết nối tới robot xảy ra lỗi: {ex.Message}"); + await Task.Delay(3000, stoppingToken); + } + } + } +} diff --git a/RobotNet.RobotManager/Services/RobotPublisher.cs b/RobotNet.RobotManager/Services/RobotPublisher.cs new file mode 100644 index 0000000..9fdb98e --- /dev/null +++ b/RobotNet.RobotManager/Services/RobotPublisher.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.HubClients; +using RobotNet.RobotManager.Hubs; + +namespace RobotNet.RobotManager.Services; + +public class RobotPublisher(RobotManager RobotManager, IHubContext RobotHub, MapHubClient MapHub, LoggerController Logger) : IHostedService +{ + public readonly Dictionary MapActive = []; + public readonly Dictionary RobotDetailActive = []; + + private int PublishCounter = 0; + private const int intervalTime = 200; + + private WatchTimerAsync? Timer; + + private readonly Dictionary ElementsState = []; + + private async Task TimerHandler() + { + try + { + foreach (var mapActive in MapActive) + { + var robotinfos = RobotManager.GetRobotInfo(mapActive.Key); + if (robotinfos is not null && robotinfos.Any()) + { + await RobotHub.Clients.Client(mapActive.Value).SendAsync("UpdateChanged", robotinfos); + } + } + + PublishCounter++; + if (PublishCounter >= 5) + { + await RobotHub.Clients.All.SendAsync("IsOnlineChanged", RobotManager.GetRobotOnlineState()); + foreach (var robotActive in RobotDetailActive) + { + var robotinfos = RobotManager.GetRobotVDA5050State(robotActive.Key); + if (robotinfos is not null) + { + await RobotHub.Clients.Client(robotActive.Value).SendAsync("VDA5050InfoChanged", robotinfos); + } + } + foreach (var mapActive in MapActive) + { + var elementsState = await MapHub.GetElementsState(mapActive.Key); + if (!elementsState.IsSuccess) Logger.Warning($"Robot Publisher Error: Lấy dữ liệu element xảy ra lỗi: {elementsState.Message}"); + else if (elementsState.Data?.Length > 0) + { + ElementsState.TryGetValue(mapActive.Key, out ElementDto[]? elementsOlder); + List updateElementsState = []; + foreach (var element in elementsState.Data) + { + if (elementsOlder is null || !elementsOlder.Any(e => e.Id == element.Id) || + elementsOlder.Any(e => e.Id == element.Id && (e.IsOpen != element.IsOpen || e.OffsetX != element.OffsetX || e.OffsetY != element.OffsetY))) + { + updateElementsState.Add(element); + } + } + if (updateElementsState is not null && updateElementsState.Count != 0) + { + ElementsState[mapActive.Key] = elementsState.Data; + await RobotHub.Clients.Client(mapActive.Value).SendAsync("ElementsStateChanged", updateElementsState); + } + } + } + PublishCounter = 0; + } + + } + catch (Exception ex) + { + Logger.Error($"Robot Publisher Error: {ex}"); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + await Task.Yield(); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + Timer = new(intervalTime, TimerHandler, Logger); + Timer.Start(); + await MapHub.StartAsync(); + break; + } + + catch (Exception ex) + { + Logger.Warning($"Robot Publisher Start: Khởi tạo kết nối với MapManager có lỗi xảy ra: {ex.Message}"); + await Task.Delay(2000, cancellationToken); + } + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Timer?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/Algorithm/FuzzyLogic.cs b/RobotNet.RobotManager/Services/Simulation/Algorithm/FuzzyLogic.cs new file mode 100644 index 0000000..26ccee6 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Algorithm/FuzzyLogic.cs @@ -0,0 +1,252 @@ +namespace RobotNet.RobotManager.Services.Simulation.Algorithm; + +public class FuzzyLogic +{ + private double Gain_P = 0.5; + private double Gain_I = 0.01; + private double DiscreteTimeIntegrator_DSTATE; + + public FuzzyLogic WithGainP(double gainP) + { + Gain_P = gainP; + return this; + } + + public FuzzyLogic WithGainI(double gainI) + { + Gain_I = gainI; + return this; + } + + private static double Fuzzy_trapmf(double x, double[] parame) + { + double b_y1; + double y2; + b_y1 = 0.0; + y2 = 0.0; + if (x >= parame[1]) + { + b_y1 = 1.0; + } + if (x < parame[0]) + { + b_y1 = 0.0; + } + if (parame[0] <= x && x < parame[1] && parame[0] != parame[1]) + { + b_y1 = 1.0 / (parame[1] - parame[0]) * (x - parame[0]); + } + if (x <= parame[2]) + { + y2 = 1.0; + } + if (x > parame[3]) + { + y2 = 0.0; + } + if (parame[2] < x && x <= parame[3] && parame[2] != parame[3]) + { + y2 = 1.0 / (parame[3] - parame[2]) * (parame[3] - x); + } + return b_y1 < y2 ? b_y1 : y2; + } + + private static double Fuzzy_trimf(double x, double[] parame) + { + double y; + y = 0.0; + if (parame[0] != parame[1] && parame[0] < x && x < parame[1]) + { + y = 1.0 / (parame[1] - parame[0]) * (x - parame[0]); + } + if (parame[1] != parame[2] && parame[1] < x && x < parame[2]) + { + y = 1.0 / (parame[2] - parame[1]) * (parame[2] - x); + } + if (x == parame[1]) + { + y = 1.0; + } + return y; + } + + public (double wl, double wr) Fuzzy_step(double v, double w, double TimeSample) + { + (double wl, double wr) result = new(); + double[] inputMFCache = new double[10]; + double[] outputMFCache = new double[5]; + double[] outputMFCache_0 = new double[5]; + double[] tmp = new double[3]; + double aggregatedOutputs; + double rtb_TmpSignalConversionAtSFun_0; + double rtb_antecedentOutputs_e; + double sumAntecedentOutputs; + int ruleID; + double[] f = [-1.0E+10, -1.0E+10, -1.0, -0.5]; + double[] e = [0.5, 1.0, 1.0E+10, 1.0E+10]; + double[] d = [0.75, 1.0, 1.0E+9, 1.0E+9]; + double[] c = [-1.0E+9, -1.0E+9, 0.0, 0.25]; + byte[] b = [ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, + 4, 4, 4, 4, 5, 5, 5, 5, 5, 1, 2, 3, 4, 5, 1, 2, 3, + 4, 5, 1, 2, 3, 4, 5, 3, 4, 5, 1, 2, 1, 2, 3, 4, 5 ]; + byte[] b_0 = [1, 1, 2, 1, 1, 2, 3, 5, 1, 4, 5, 5, 5, 5, 5, 2, 1, 1, 1, 1, 5, 5, 5, 5, 5]; + byte[] b_1 = [ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, + 4, 4, 4, 4, 5, 5, 5, 5, 5, 1, 2, 3, 4, 5, 4, 1, + 2, 3, 5, 3, 1, 2, 4, 5, 1, 2, 3, 4, 5, 1, 2, 4, 5, 3 ]; + byte[] b_2 = [5, 5, 5, 5, 5, 1, 2, 3, 5, 4, 2, 1, 1, 1, 1, 5, 5, 5, 5, 5, 1, 1, 1, 1, 2]; + double inputMFCache_tmp; + double inputMFCache_tmp_0; + double inputMFCache_tmp_1; + double inputMFCache_tmp_2; + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller' */ + /* SignalConversion generated from: '/ SFunction ' incorporates: + * Constant: '/w' + * DiscreteIntegrator: '/Discrete-Time Integrator' + * Gain: '/Gain1' + * MATLAB Function: '/Evaluate Rule Antecedents' + * MATLAB Function: '/Evaluate Rule Antecedents' + * SignalConversion generated from: '/ SFunction ' + * Sum: '/Sum' + */ + DiscreteTimeIntegrator_DSTATE += Gain_I * w * TimeSample; + rtb_TmpSignalConversionAtSFun_0 = Gain_P * w + DiscreteTimeIntegrator_DSTATE; + /* End of Outputs for SubSystem: '/Fuzzy Logic Controller1' */ + /* MATLAB Function: '/Evaluate Rule Antecedents' incorporates: + * Constant: '/v' + * MATLAB Function: '/Evaluate Rule Antecedents' + * SignalConversion generated from: '/ SFunction ' + */ + sumAntecedentOutputs = 0.0; + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache_tmp = Fuzzy_trapmf(rtb_TmpSignalConversionAtSFun_0, f); + /* End of Outputs for SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache[0] = inputMFCache_tmp; + tmp[0] = -0.5; + tmp[1] = 0.0; + tmp[2] = 0.5; + inputMFCache[1] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache_tmp_0 = Fuzzy_trapmf(rtb_TmpSignalConversionAtSFun_0, e); + /* End of Outputs for SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache[2] = inputMFCache_tmp_0; + tmp[0] = -1.0; + tmp[1] = -0.5; + tmp[2] = 0.0; + inputMFCache[3] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + tmp[0] = 0.0; + tmp[1] = 0.5; + tmp[2] = 1.0; + inputMFCache[4] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + tmp[0] = 0.0; + tmp[1] = 0.25; + tmp[2] = 0.5; + inputMFCache[5] = Fuzzy_trimf(v, tmp); + tmp[0] = 0.25; + tmp[1] = 0.5; + tmp[2] = 0.75; + inputMFCache[6] = Fuzzy_trimf(v, tmp); + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache_tmp_1 = Fuzzy_trapmf(v, d); + /* End of Outputs for SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache[7] = inputMFCache_tmp_1; + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache_tmp_2 = Fuzzy_trapmf(v, c); + /* End of Outputs for SubSystem: '/Fuzzy Logic Controller1' */ + inputMFCache[8] = inputMFCache_tmp_2; + tmp[0] = 0.5; + tmp[1] = 0.75; + tmp[2] = 1.0; + inputMFCache[9] = Fuzzy_trimf(v, tmp); + /* MATLAB Function: '/Evaluate Rule Consequents' */ + aggregatedOutputs = 0.0; + outputMFCache[0] = 0.0; + outputMFCache[1] = 0.25; + outputMFCache[2] = 0.5; + outputMFCache[3] = 0.75; + outputMFCache[4] = 1.0; + for (ruleID = 0; ruleID < 25; ruleID++) + { + /* MATLAB Function: '/Evaluate Rule Antecedents' */ + rtb_antecedentOutputs_e = inputMFCache[b[ruleID + 25] + 4] * inputMFCache[b[ruleID] - 1]; + sumAntecedentOutputs += rtb_antecedentOutputs_e; + /* MATLAB Function: '/Evaluate Rule Consequents' */ + aggregatedOutputs += outputMFCache[b_0[ruleID] - 1] * rtb_antecedentOutputs_e; + } + /* MATLAB Function: '/Defuzzify Outputs' incorporates: + * MATLAB Function: '/Evaluate Rule Antecedents' + * MATLAB Function: '/Evaluate Rule Consequents' + */ + if (sumAntecedentOutputs == 0.0) + { + result.wr = 0.5; + } + else + { + result.wr = 1.0 / sumAntecedentOutputs * aggregatedOutputs; + } + /* Outputs for Atomic SubSystem: '/Fuzzy Logic Controller1' */ + /* MATLAB Function: '/Evaluate Rule Antecedents' incorporates: + * Constant: '/v' + * SignalConversion generated from: '/ SFunction ' + */ + sumAntecedentOutputs = 0.0; + inputMFCache[0] = inputMFCache_tmp; + tmp[0] = -0.5; + tmp[1] = 0.0; + tmp[2] = 0.5; + inputMFCache[1] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + inputMFCache[2] = inputMFCache_tmp_0; + tmp[0] = -1.0; + tmp[1] = -0.5; + tmp[2] = 0.0; + inputMFCache[3] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + tmp[0] = 0.0; + tmp[1] = 0.5; + tmp[2] = 1.0; + inputMFCache[4] = Fuzzy_trimf(rtb_TmpSignalConversionAtSFun_0, tmp); + tmp[0] = 0.0; + tmp[1] = 0.25; + tmp[2] = 0.5; + inputMFCache[5] = Fuzzy_trimf(v, tmp); + tmp[0] = 0.25; + tmp[1] = 0.5; + tmp[2] = 0.75; + inputMFCache[6] = Fuzzy_trimf(v, tmp); + inputMFCache[7] = inputMFCache_tmp_1; + inputMFCache[8] = inputMFCache_tmp_2; + tmp[0] = 0.5; + tmp[1] = 0.75; + tmp[2] = 1.0; + inputMFCache[9] = Fuzzy_trimf(v, tmp); + /* MATLAB Function: '/Evaluate Rule Consequents' */ + aggregatedOutputs = 0.0; + outputMFCache_0[0] = 0.0; + outputMFCache_0[1] = 0.25; + outputMFCache_0[2] = 0.5; + outputMFCache_0[3] = 0.75; + outputMFCache_0[4] = 1.0; + for (ruleID = 0; ruleID < 25; ruleID++) + { + /* MATLAB Function: '/Evaluate Rule Antecedents' */ + rtb_antecedentOutputs_e = inputMFCache[b_1[ruleID + 25] + 4] * inputMFCache[b_1[ruleID] - 1]; + sumAntecedentOutputs += rtb_antecedentOutputs_e; + /* MATLAB Function: '/Evaluate Rule Consequents' */ + aggregatedOutputs += outputMFCache_0[b_2[ruleID] - 1] * + rtb_antecedentOutputs_e; + } + /* MATLAB Function: '/Defuzzify Outputs' incorporates: + * MATLAB Function: '/Evaluate Rule Antecedents' + * MATLAB Function: '/Evaluate Rule Consequents' + */ + if (sumAntecedentOutputs == 0.0) + { + result.wl = 0.5; + } + else + { + result.wl = 1.0 / sumAntecedentOutputs * aggregatedOutputs; + } + return result; + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/Algorithm/MathExtension.cs b/RobotNet.RobotManager/Services/Simulation/Algorithm/MathExtension.cs new file mode 100644 index 0000000..a705723 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Algorithm/MathExtension.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotManager.Services.Simulation.Algorithm; + +public class MathExtension +{ + public static double ToRad => Math.PI / 180; + public static double ToDegree => 180 / Math.PI; + public static double CheckLimit(double value, double Max, double Min) => value < Max ? value > Min ? value : Min : Max; +} diff --git a/RobotNet.RobotManager/Services/Simulation/Algorithm/PID.cs b/RobotNet.RobotManager/Services/Simulation/Algorithm/PID.cs new file mode 100644 index 0000000..f6398ba --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Algorithm/PID.cs @@ -0,0 +1,42 @@ +namespace RobotNet.RobotManager.Services.Simulation.Algorithm; + +public class PID +{ + private double Kp = 0.3; + private double Ki = 0.0001; + private double Kd = 0.01; + private double Pre_Error; + private double Pre_Pre_Error; + private double Pre_Out; + + public PID WithKp(double kp) + { + Kp = kp; + return this; + } + + public PID WithKi(double ki) + { + Ki = ki; + return this; + } + + public PID WithKd(double kd) + { + Kd = kd; + return this; + } + + public double PID_step(double Error, double LimitMax, double LimitMin, double TimeSample) + { + double P_part = Kp * (Error - Pre_Error); + double I_part = 0.5 * Ki * TimeSample * (Error + Pre_Error); + double D_part = Kd / TimeSample * (Error - 2 * Pre_Error + Pre_Pre_Error); + double Out = Pre_Out + P_part + I_part + D_part; + Pre_Pre_Error = Pre_Error; + Pre_Error = Error; + Pre_Out = Out; + Out = MathExtension.CheckLimit(Out, LimitMax, LimitMin); + return Out; + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/Algorithm/PurePursuit.cs b/RobotNet.RobotManager/Services/Simulation/Algorithm/PurePursuit.cs new file mode 100644 index 0000000..2cdb90b --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Algorithm/PurePursuit.cs @@ -0,0 +1,159 @@ +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Simulation.Algorithm; +public class PurePursuit +{ + private double MaxAngularVelocity = 1.5; + private double LookaheadDistance = 0.5; + private List Waypoints_Value = []; + public int OnNodeIndex = 0; + public int GoalIndex = 0; + public NavigationNode? Goal; + + public PurePursuit WithLookheadDistance(double distance) + { + LookaheadDistance = distance; + return this; + } + + public PurePursuit WithMaxAngularVelocity(double vel) + { + MaxAngularVelocity = vel; + return this; + } + + public PurePursuit WithPath(NavigationNode[] path) + { + Waypoints_Value = [.. path]; + return this; + } + + public void UpdatePath(NavigationNode[] path) + { + Waypoints_Value = [.. path]; + } + + public void UpdateGoal(NavigationNode goal) + { + Goal = goal; + GoalIndex = Waypoints_Value.IndexOf(Goal); + } + + public (NavigationNode node, int index) GetOnNode(double x, double y) + { + double minDistance = double.MaxValue; + NavigationNode onNode = Waypoints_Value[0]; + int index = 0; + for (int i = 1; i < Waypoints_Value.Count; i++) + { + var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2)); + if (distance < minDistance) + { + onNode = Waypoints_Value[i]; + minDistance = distance; + index = i; + } + } + return (onNode, index); + } + + private (NavigationNode? node, int index) OnNode(double x, double y) + { + if (Waypoints_Value is null || Waypoints_Value.Count == 0) return (null, 0); + double minDistance = double.MaxValue; + int index = 0; + NavigationNode? onNode = null; + if(Goal is null) return (onNode, index); + for (int i = OnNodeIndex; i < Waypoints_Value.IndexOf(Goal); i ++) + { + var distance = Math.Sqrt(Math.Pow(x - Waypoints_Value[i].X, 2) + Math.Pow(y - Waypoints_Value[i].Y, 2)); + if (distance < minDistance) + { + onNode = Waypoints_Value[i]; + minDistance = distance; + index = i; + } + } + return (onNode, index); + } + + public double PurePursuit_step(double X_Ref, double Y_Ref, double Angle_Ref) + { + if (Waypoints_Value is null || Waypoints_Value.Count < 2) return 0; + NavigationNode? lookaheadStartPt = null; + var (onNode, index) = OnNode(X_Ref, Y_Ref); + if (onNode is null || Goal is null) return 0; + OnNodeIndex = index; + double lookDistance = 0; + for (int i = OnNodeIndex + 1; i < Waypoints_Value.IndexOf(Goal); i++) + { + lookDistance += Math.Sqrt(Math.Pow(Waypoints_Value[i - 1].X - Waypoints_Value[i].X, 2) + Math.Pow(Waypoints_Value[i - 1].Y - Waypoints_Value[i].Y, 2)); + if (lookDistance >= LookaheadDistance || Waypoints_Value[i].Direction != onNode.Direction) + { + lookaheadStartPt = Waypoints_Value[i]; + break; + } + } + lookaheadStartPt ??= Goal; + if (onNode.Direction == RobotDirection.BACKWARD) + { + if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2; + else if (Angle_Ref < -Math.PI) Angle_Ref += Math.PI * 2; + Angle_Ref += Math.PI; + if (Angle_Ref > Math.PI) Angle_Ref -= Math.PI * 2; + } + var distance = Math.Atan2(lookaheadStartPt.Y - Y_Ref, lookaheadStartPt.X - X_Ref) - Angle_Ref; + + if (Math.Abs(distance) > Math.PI) + { + double minDistance; + if (distance + Math.PI == 0.0) minDistance = 0.0; + else + { + double data = (distance + Math.PI) / (2 * Math.PI); + if (data < 0) data = Math.Round(data + 0.5); + else data = Math.Round(data - 0.5); + minDistance = distance + Math.PI - data * (2 * Math.PI); + double checker = 0; + if (minDistance != 0.0) + { + checker = Math.Abs((distance + Math.PI) / (2 * Math.PI)); + } + if (!(Math.Abs(checker - Math.Floor(checker + 0.5)) > 2.2204460492503131E-16 * checker)) + { + minDistance = 0.0; + } + else if (distance + Math.PI < 0.0) + { + minDistance += Math.PI * 2; + } + } + if (minDistance == 0.0 && distance + Math.PI > 0.0) + { + minDistance = Math.PI * 2; + } + distance = minDistance - Math.PI; + } + + var AngularVelocity = 2.0 * 0.5 * Math.Sin(distance) / LookaheadDistance; + if (Math.Abs(AngularVelocity) > MaxAngularVelocity) + { + if (AngularVelocity < 0.0) + { + AngularVelocity = -1.0; + } + else if (AngularVelocity > 0.0) + { + AngularVelocity = 1.0; + } + else if (AngularVelocity == 0.0) + { + AngularVelocity = 0.0; + } + AngularVelocity *= MaxAngularVelocity; + } + return AngularVelocity; + } + +} diff --git a/RobotNet.RobotManager/Services/Simulation/DifferentialNavigationService.cs b/RobotNet.RobotManager/Services/Simulation/DifferentialNavigationService.cs new file mode 100644 index 0000000..9552e70 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/DifferentialNavigationService.cs @@ -0,0 +1,169 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Simulation.Algorithm; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class DifferentialNavigationService(VisualizationService visualization, RobotSimulationModel model, IServiceProvider ServiceProvider) : NavigationService(visualization, model, ServiceProvider) +{ + public override MessageResult Rotate(double angle) + { + RotatePID = new PID().WithKp(10).WithKi(0.01).WithKd(0.1); + TargetAngle = MathExtension.CheckLimit(angle, 180, -180); + Action = NavigationAction.Rotate; + NavStart(); + CheckingStart(); + return new(true); + } + + public override MessageResult MoveStraight(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [NavigationPath[0], NavigationPath[^1]]; + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath(path); + double Angle = Math.Atan2(path[1].Y - path[0].Y, path[1].X - path[0].X); + Rotate(Angle * 180 / Math.PI); + UpdateGoal(NavigationPath[^1]); + FinalGoal = NavigationPath[^1]; + return new(true); + } + + public override MessageResult Move(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + if (CheckingNodes.Count < 2) return new(false, "Đường dẫn traffic không hợp lệ"); + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + double Angle = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X); + Rotate(Angle * 180 / Math.PI); + return new(true); + } + + public override MessageResult Move(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + var pathSplit = PathPlanner.PathSplit(nodes, edges); + if (!pathSplit.IsSuccess) return new(false, pathSplit.Message); + if (pathSplit.Data is null) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [..pathSplit.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + InNode = CheckingNodes[0]; + FinalGoal = CheckingNodes[^1]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + + double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; + double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; + Rotate(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward); + return new(true); + } + + protected override void NavigationHandler() + { + try + { + if (Action == NavigationAction.Rotate) + { + if (RotatePID is not null) + { + NavigationState = NavigationStateType.Rotating; + double Error = Visualization.Theta - TargetAngle; + if (Error > 180) Error -= 360; + else if (Error < -180) Error += 360; + if (Math.Abs(Error) < 1) + { + if (NavigationPath is not null) Action = NavigationAction.Move; + else Dispose(); + } + else + { + var SpeedCal = RotatePID.PID_step(Error * MathExtension.ToRad, AngularVelocity, -AngularVelocity, SampleTimeMilliseconds / 1000.0); + VelocityController.SetSpeed(SpeedCal, SpeedCal); + } + } + } + else if (Action == NavigationAction.Move) + { + if (NavigationPath is not null && NavigationGoal is not null) + { + if (MovePID is not null && MoveFuzzy is not null && MovePurePursuit is not null && FinalGoal is not null) + { + var DistanceToGoal = Math.Sqrt(Math.Pow(Visualization.X - FinalGoal.X, 2) + Math.Pow(Visualization.Y - FinalGoal.Y, 2)); + var DistanceToCheckingNode = Math.Sqrt(Math.Pow(Visualization.X - NavigationGoal.X, 2) + Math.Pow(Visualization.Y - NavigationGoal.Y, 2)); + var deviation = NavigationGoal.Id == FinalGoal.Id ? 0.02 : 0.1; + if (DistanceToCheckingNode > deviation) + { + NavigationState = NavigationStateType.Moving; + double SpeedTarget = MovePID.PID_step(DistanceToCheckingNode, RobotModel.MaxVelocity, 0, SampleTimeMilliseconds / 1000.0); + double AngularVel = MovePurePursuit.PurePursuit_step(Visualization.X, Visualization.Y, Visualization.Theta * Math.PI / 180); + AngularVel *= NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD ? 1 : -1; + (double AngularVelocityLeft, double AngularVelocityRight) = MoveFuzzy.Fuzzy_step(SpeedTarget, AngularVel, SampleTimeMilliseconds / 1000.0); + + //AngularVelocityLeft /= RobotModel.RadiusWheel; + //AngularVelocityRight = AngularVelocityRight / RobotModel.RadiusWheel * -1; + if (NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD) + { + AngularVelocityLeft /= RobotModel.RadiusWheel; + AngularVelocityRight = AngularVelocityRight / RobotModel.RadiusWheel * -1; + } + else + { + AngularVelocityLeft = AngularVelocityLeft / RobotModel.RadiusWheel * -1; + AngularVelocityRight /= RobotModel.RadiusWheel; + } + VelocityController.SetSpeed(AngularVelocityLeft, AngularVelocityRight); + if (MovePurePursuit.OnNodeIndex < NavigationPath.Count) + { + var inNode = NavigationPath[MovePurePursuit.OnNodeIndex]; + if (CheckingNodes.Any(n => n.Id == inNode.Id)) InNode = inNode; + else + { + var inNodeIndex = CheckingNodes.IndexOf(InNode ?? new()); + inNodeIndex = inNodeIndex < 0 ? 0 : inNodeIndex; + for (int i = inNodeIndex; i < CheckingNodes.Count; i++) + { + if (Math.Sqrt(Math.Pow(CheckingNodes[i].X - inNode.X, 2) + Math.Pow(CheckingNodes[i].Y - inNode.Y, 2)) < 0.2) + { + InNode = CheckingNodes[i]; + break; + } + } + } + } + } + else if (DistanceToGoal < 0.02) Dispose(); + else NavigationState = NavigationStateType.Waitting; + } + } + } + } + catch (Exception ex) + { + Logger.Warning($"{RobotModel.RobotId} Nav ex: {ex.Message}"); + } + } + +} diff --git a/RobotNet.RobotManager/Services/Simulation/ForkliftNavigationSevice.cs b/RobotNet.RobotManager/Services/Simulation/ForkliftNavigationSevice.cs new file mode 100644 index 0000000..cd5d544 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/ForkliftNavigationSevice.cs @@ -0,0 +1,140 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Simulation.Algorithm; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class ForkliftNavigationSevice(VisualizationService visualization, RobotSimulationModel model, IServiceProvider ServiceProvider) : NavigationService(visualization, model, ServiceProvider) +{ + public override MessageResult Rotate(double angle) + { + return new(false, "Robot không có chức năng này"); + } + + public override MessageResult MoveStraight(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [NavigationPath[0], NavigationPath[^1]]; + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.15).WithPath(path); + + UpdateGoal(NavigationPath[^1]); + FinalGoal = NavigationPath[^1]; + NavStart(); + return new(true); + } + + + public override MessageResult Move(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + if (CheckingNodes.Count < 2) return new(false, "Đường dẫn traffic không hợp lệ"); + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.2).WithPath(path); + + NavStart(); + CheckingStart(); + return new(true); + } + + public override MessageResult Move(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + var pathSplit = PathPlanner.PathSplit(nodes, edges); + if (!pathSplit.IsSuccess) return new(false, pathSplit.Message); + if (pathSplit.Data is null) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [..pathSplit.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + InNode = CheckingNodes[0]; + FinalGoal = CheckingNodes[^1]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + + NavStart(); + CheckingStart(); + return new(true); + } + + protected override void NavigationHandler() + { + try + { + if (NavigationPath is not null && NavigationGoal is not null) + { + if (MovePID is not null && MoveFuzzy is not null && MovePurePursuit is not null && FinalGoal is not null) + { + var DistanceToGoal = Math.Sqrt(Math.Pow(Visualization.X - FinalGoal.X, 2) + Math.Pow(Visualization.Y - FinalGoal.Y, 2)); + var DistanceToCheckingNode = Math.Sqrt(Math.Pow(Visualization.X - NavigationGoal.X, 2) + Math.Pow(Visualization.Y - NavigationGoal.Y, 2)); + var deviation = NavigationGoal.Id == FinalGoal.Id ? 0.02 : 0.1; + if (DistanceToCheckingNode > deviation) + { + NavigationState = NavigationStateType.Moving; + double SpeedTarget = MovePID.PID_step(DistanceToCheckingNode, RobotModel.MaxVelocity, 0, SampleTimeMilliseconds / 1000.0); + double AngularVel = MovePurePursuit.PurePursuit_step(Visualization.X, Visualization.Y, Visualization.Theta * Math.PI / 180); + + AngularVel *= NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD ? 1 : -1; + (double AngularVelocityLeft, double AngularVelocityRight) = MoveFuzzy.Fuzzy_step(SpeedTarget, AngularVel, SampleTimeMilliseconds / 1000.0); + + if (NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD) + { + AngularVelocityLeft /= RobotModel.RadiusWheel; + AngularVelocityRight = AngularVelocityRight / RobotModel.RadiusWheel * -1; + } + else + { + AngularVelocityLeft = AngularVelocityLeft / RobotModel.RadiusWheel * -1; + AngularVelocityRight /= RobotModel.RadiusWheel; + } + VelocityController.SetSpeed(AngularVelocityLeft, AngularVelocityRight); + if (MovePurePursuit.OnNodeIndex < NavigationPath.Count) + { + var inNode = NavigationPath[MovePurePursuit.OnNodeIndex]; + if (CheckingNodes.Any(n => n.Id == inNode.Id)) InNode = inNode; + else + { + var inNodeIndex = CheckingNodes.IndexOf(InNode ?? new()); + inNodeIndex = inNodeIndex < 0 ? 0 : inNodeIndex; + for (int i = inNodeIndex; i < CheckingNodes.Count; i++) + { + if (Math.Sqrt(Math.Pow(CheckingNodes[i].X - inNode.X, 2) + Math.Pow(CheckingNodes[i].Y - inNode.Y, 2)) < 0.2) + { + InNode = CheckingNodes[i]; + break; + } + } + } + } + } + else if (DistanceToGoal < 0.02) Dispose(); + else NavigationState = NavigationStateType.Waitting; + } + } + } + catch (Exception ex) + { + Logger.Warning($"{RobotModel.RobotId} Nav ex: {ex.Message}"); + } + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/GridDifferentialNavigationService.cs b/RobotNet.RobotManager/Services/Simulation/GridDifferentialNavigationService.cs new file mode 100644 index 0000000..803f2f3 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/GridDifferentialNavigationService.cs @@ -0,0 +1,172 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Simulation.Algorithm; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class GridDifferentialNavigationService(VisualizationService visualization, RobotSimulationModel model, IServiceProvider ServiceProvider) : NavigationService(visualization, model, ServiceProvider) +{ + public override MessageResult Rotate(double angle) + { + RotatePID = new PID().WithKp(10).WithKi(0.01).WithKd(0.1); + TargetAngle = MathExtension.CheckLimit(angle, 180, -180); + Action = NavigationAction.Rotate; + NavStart(); + CheckingStart(); + return new(true); + } + + public override MessageResult MoveStraight(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [NavigationPath[0], NavigationPath[^1]]; + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath(path); + double Angle = Math.Atan2(path[1].Y - path[0].Y, path[1].X - path[0].X); + Rotate(Angle * 180 / Math.PI); + UpdateGoal(NavigationPath[^1]); + FinalGoal = NavigationPath[^1]; + return new(true); + } + + public override MessageResult Move(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + if (CheckingNodes.Count < 2) return new(false, "Đường dẫn traffic không hợp lệ"); + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + double Angle = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X); + Rotate(Angle * 180 / Math.PI); + return new(true); + } + + public override MessageResult Move(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + var pathNodes = PathPlanner.CalculatorDirection(MapCompute.GetRobotDirection(nodes[0].Direction), nodes, edges); + var pathSplit = PathPlanner.PathSplit(pathNodes, edges); + if (!pathSplit.IsSuccess) return new(false, pathSplit.Message); + if (pathSplit.Data is null) return new(false, "Đường dẫn không hợp lệ"); + + var getZones = PathPlanner.GetZones(MapId, nodes); + getZones.Wait(); + Zones = getZones.Result.Data ?? []; + + NavigationPath = [..pathSplit.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + InNode = CheckingNodes[0]; + FinalGoal = CheckingNodes[^1]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + + double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; + double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; + Rotate(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward); + return new(true); + } + + protected override void NavigationHandler() + { + try + { + if (Action == NavigationAction.Rotate) + { + if (RotatePID is not null) + { + NavigationState = NavigationStateType.Rotating; + double Error = Visualization.Theta - TargetAngle; + if (Error > 180) Error -= 360; + else if (Error < -180) Error += 360; + if (Math.Abs(Error) < 1) + { + if (NavigationPath is not null) Action = NavigationAction.Move; + else Dispose(); + } + else + { + var SpeedCal = RotatePID.PID_step(Error * MathExtension.ToRad, AngularVelocity, -AngularVelocity, SampleTimeMilliseconds / 1000.0); + VelocityController.SetSpeed(SpeedCal, SpeedCal); + } + } + } + else if (Action == NavigationAction.Move) + { + if (NavigationPath is not null && NavigationGoal is not null) + { + if (MovePID is not null && MoveFuzzy is not null && MovePurePursuit is not null && FinalGoal is not null) + { + var DistanceToGoal = Math.Sqrt(Math.Pow(Visualization.X - FinalGoal.X, 2) + Math.Pow(Visualization.Y - FinalGoal.Y, 2)); + var DistanceToNavigationGoal = Math.Sqrt(Math.Pow(Visualization.X - NavigationGoal.X, 2) + Math.Pow(Visualization.Y - NavigationGoal.Y, 2)); + var deviation = NavigationGoal.Id == FinalGoal.Id ? 0.02 : 0.05; + if (DistanceToNavigationGoal > deviation) + { + NavigationState = NavigationStateType.Moving; + double SpeedTarget = MovePID.PID_step(DistanceToNavigationGoal, RobotModel.MaxVelocity, 0, SampleTimeMilliseconds / 1000.0); + double AngularVel = MovePurePursuit.PurePursuit_step(Visualization.X, Visualization.Y, Visualization.Theta * Math.PI / 180); + AngularVel *= NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD ? 1 : -1; + (double AngularVelocityLeft, double AngularVelocityRight) = MoveFuzzy.Fuzzy_step(SpeedTarget, AngularVel, SampleTimeMilliseconds / 1000.0); + + if (NavigationPath[MovePurePursuit.OnNodeIndex].Direction == RobotDirection.FORWARD) + { + AngularVelocityLeft /= RobotModel.RadiusWheel; + AngularVelocityRight = AngularVelocityRight / RobotModel.RadiusWheel * -1; + } + else + { + AngularVelocityLeft = AngularVelocityLeft / RobotModel.RadiusWheel * -1; + AngularVelocityRight /= RobotModel.RadiusWheel; + } + VelocityController.SetSpeed(AngularVelocityLeft, AngularVelocityRight); + if (MovePurePursuit.OnNodeIndex < NavigationPath.Count) + { + var inNode = NavigationPath[MovePurePursuit.OnNodeIndex]; + if (CheckingNodes.Any(n => n.Id == inNode.Id)) InNode = inNode; + else + { + var inNodeIndex = CheckingNodes.IndexOf(InNode ?? new()); + inNodeIndex = inNodeIndex < 0 ? 0 : inNodeIndex; + for (int i = inNodeIndex; i < CheckingNodes.Count; i++) + { + if (Math.Sqrt(Math.Pow(CheckingNodes[i].X - inNode.X, 2) + Math.Pow(CheckingNodes[i].Y - inNode.Y, 2)) < 0.2) + { + InNode = CheckingNodes[i]; + break; + } + } + } + } + } + else if (DistanceToGoal < 0.02) Dispose(); + else NavigationState = NavigationStateType.Waitting; + } + } + } + } + catch (Exception ex) + { + Logger.Warning($"{RobotModel.RobotId} Nav ex: {ex.Message}"); + } + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/INavigationService.cs b/RobotNet.RobotManager/Services/Simulation/INavigationService.cs new file mode 100644 index 0000000..13bdb50 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/INavigationService.cs @@ -0,0 +1,18 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Simulation; + +public interface INavigationService : IRobotOrder, IDisposable +{ + Guid MapId { get; set; } + NavigationNode? InNode { get; } + List CurrentZones { get; set; } + NavigationStateType NavigationState { get; } + MessageResult Rotate(double angle); + MessageResult Move(NavigationNode[] path); + MessageResult Move(NodeDto[] nodes, EdgeDto[] edges); + MessageResult MoveStraight(NavigationNode[] path); + MessageResult Cancel(); +} diff --git a/RobotNet.RobotManager/Services/Simulation/Models/NavigationAction.cs b/RobotNet.RobotManager/Services/Simulation/Models/NavigationAction.cs new file mode 100644 index 0000000..18405e5 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Models/NavigationAction.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotManager.Services.Simulation.Models; + +public enum NavigationAction +{ + Rotate, + Move, + Start, + Stop, +} diff --git a/RobotNet.RobotManager/Services/Simulation/Models/NavigationNode.cs b/RobotNet.RobotManager/Services/Simulation/Models/NavigationNode.cs new file mode 100644 index 0000000..382fc50 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Models/NavigationNode.cs @@ -0,0 +1,40 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Simulation.Models; + +public class NavigationNode +{ + public Guid Id { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public RobotDirection Direction { get; set; } + public string Actions { get; set; } = string.Empty; + + public override bool Equals(object? obj) + { + if (obj is NavigationNode other) + return Id == other.Id; + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Id); + } + + public NodeDto ToNodeDto() + { + return new NodeDto + { + Id = Id, + X = X, + Y = Y, + Theta = Theta, + Direction = MapCompute.GetNodeDirection(Direction), + Actions = Actions + }; + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/Models/NavigationStateType.cs b/RobotNet.RobotManager/Services/Simulation/Models/NavigationStateType.cs new file mode 100644 index 0000000..4c7b4dd --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Models/NavigationStateType.cs @@ -0,0 +1,12 @@ +namespace RobotNet.RobotManager.Services.Simulation.Models; + +public enum NavigationStateType +{ + None, + Ready, + Moving, + Stop, + Error, + Rotating, + Waitting, +} diff --git a/RobotNet.RobotManager/Services/Simulation/Models/RobotSimulationModel.cs b/RobotNet.RobotManager/Services/Simulation/Models/RobotSimulationModel.cs new file mode 100644 index 0000000..a8cca69 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/Models/RobotSimulationModel.cs @@ -0,0 +1,16 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Simulation.Models; + +public class RobotSimulationModel +{ + public string RobotId { get; set; } = string.Empty; + public double RadiusWheel { get; set; } + public double Width { get; set; } + public double Length { get; set; } + public double MaxVelocity { get; set; } + public double MaxAngularVelocity { get; set; } + public double Acceleration { get; set; } + public double Deceleration { get; set; } + public NavigationType NavigationType { get; set; } +} diff --git a/RobotNet.RobotManager/Services/Simulation/NavigationManager.cs b/RobotNet.RobotManager/Services/Simulation/NavigationManager.cs new file mode 100644 index 0000000..fba1da7 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/NavigationManager.cs @@ -0,0 +1,15 @@ +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class NavigationManager +{ + public static INavigationService GetNavigation(VisualizationService Visualization, RobotSimulationModel RobotModel, IServiceProvider ServiceProvider) + { + if (RobotModel.NavigationType == NavigationType.Forklift) return new ForkliftNavigationSevice(Visualization, RobotModel, ServiceProvider); + else if(RobotModel.NavigationType == NavigationType.GridDifferential) return new GridDifferentialNavigationService(Visualization, RobotModel, ServiceProvider); + return new DifferentialNavigationService(Visualization, RobotModel, ServiceProvider); + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/NavigationService.cs b/RobotNet.RobotManager/Services/Simulation/NavigationService.cs new file mode 100644 index 0000000..e549ece --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/NavigationService.cs @@ -0,0 +1,655 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.OpenACS; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Simulation.Algorithm; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; + +namespace RobotNet.RobotManager.Services.Simulation; + +public abstract class NavigationService : INavigationService, IDisposable +{ + protected readonly VisualizationService Visualization; + protected readonly VelocityController VelocityController; + protected readonly RobotSimulationModel RobotModel; + protected readonly TrafficManager TrafficManager; + protected readonly TrafficACS TrafficACS; + protected readonly PathPlanner PathPlanner; + protected readonly MapManager MapManager; + protected readonly LoggerController Logger; + + private WatchTimer? navTimer; + protected const int SampleTimeMilliseconds = 50; + + private WatchTimerAsync? checkingTimer; + private const int CheckingTimeMilliseconds = 1000; + + protected double TargetAngle = 0; + protected PID? RotatePID; + protected readonly double AngularVelocity; + + protected PID? MovePID; + protected FuzzyLogic? MoveFuzzy; + protected PurePursuit? MovePurePursuit; + + private Guid TrafficManagerGoalId = Guid.Empty; + private Guid TrafficACSGoalId = Guid.Empty; + protected NavigationNode? NavigationGoal; + protected NavigationNode? FinalGoal; + protected List? NavigationPath; + protected List CheckingNodes = []; + private List RefreshPath = []; + private List BaseNodes = []; + private bool IsWaittingStop; + private double RotateAngle; + protected Dictionary Zones = []; + + protected NavigationAction Action = NavigationAction.Stop; + + public NavigationNode? InNode { get; set; } = null; + public NavigationStateType NavigationState { get; set; } = NavigationStateType.Ready; + public TrafficSolutionState TrafficSolutionState { get; private set; } = TrafficSolutionState.None; + public bool IsError { get; private set; } + public bool IsCompleted { get; private set; } = true; + public bool IsProcessing { get; private set; } + public bool IsCanceled { get; private set; } + public string[] Errors => []; + public NavigationPathEdge[] FullPath => GetFullPath(); + public NavigationPathEdge[] BasePath => GetBasePath(); + public Guid MapId { get; set; } = Guid.Empty; + public List CurrentZones { get; set; } = []; + + public NavigationService(VisualizationService visualization, RobotSimulationModel model, IServiceProvider ServiceProvider) + { + Visualization = visualization; + VelocityController = new VelocityController(Visualization).WithDeceleration(model.Deceleration).WithAcceleration(model.Acceleration).WithSampleTime(SampleTimeMilliseconds / 1000.0); + RobotModel = model; + AngularVelocity = MathExtension.CheckLimit(RobotModel.MaxAngularVelocity * RobotModel.Width / RobotModel.RadiusWheel, 2 * Math.PI, 0); + TrafficManager = ServiceProvider.GetRequiredService(); + TrafficACS = ServiceProvider.GetRequiredService(); + PathPlanner = ServiceProvider.GetRequiredService(); + MapManager = ServiceProvider.GetRequiredService(); + Logger = ServiceProvider.GetRequiredService>(); + } + + public virtual MessageResult Rotate(double angle) + { + RotatePID = new PID().WithKp(10).WithKi(0.01).WithKd(0.1); + TargetAngle = MathExtension.CheckLimit(angle, 180, -180); + Action = NavigationAction.Rotate; + NavStart(); + CheckingStart(); + return new(true); + } + + public virtual MessageResult MoveStraight(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [NavigationPath[0], NavigationPath[^1]]; + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath(path); + double Angle = Math.Atan2(path[1].Y - path[0].Y, path[1].X - path[0].X); + Rotate(Angle * 180 / Math.PI); + return new(true); + } + + public virtual MessageResult Move(NavigationNode[] path) + { + if (path.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + NavigationPath = [.. path]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + if (CheckingNodes.Count < 2) return new(false, "Đường dẫn traffic không hợp lệ"); + InNode = CheckingNodes[0]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + double Angle = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X); + Rotate(Angle * 180 / Math.PI); + return new(true); + } + + public virtual MessageResult Move(NodeDto[] nodes, EdgeDto[] edges) + { + if (nodes.Length < 2) return new(false, "Đường dẫn không hợp lệ"); + var pathNodes = PathPlanner.CalculatorDirection(MapCompute.GetRobotDirection(nodes[0].Direction), nodes, edges); + var pathSplit = PathPlanner.PathSplit(pathNodes, edges); + if (!pathSplit.IsSuccess) return new(false, pathSplit.Message); + if (pathSplit.Data is null) return new(false, "Đường dẫn không hợp lệ"); + + var getZones = PathPlanner.GetZones(MapId, nodes); + getZones.Wait(); + Zones = getZones.Result.Data ?? []; + + NavigationPath = [..pathSplit.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + InNode = CheckingNodes[0]; + FinalGoal = CheckingNodes[^1]; + + MovePID = new PID().WithKp(1).WithKi(0.0001).WithKd(0.6); + MoveFuzzy = new FuzzyLogic().WithGainP(1.1); + MovePurePursuit = new PurePursuit().WithLookheadDistance(0.35).WithPath([.. NavigationPath]); + + double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; + double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; + Rotate(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward); + return new(true); + } + + public MessageResult Cancel() + { + Dispose(); + VelocityController.Stop(); + NavigationState = NavigationStateType.Ready; + IsCanceled = true; + return new(true); + } + + protected virtual void NavigationHandler() + { + } + + private void HandleRefreshPath(TrafficSolution trafficSolution) + { + var edgesDto = trafficSolution.Edges.Select(e => new EdgeDto + { + Id = e.Id, + StartNodeId = e.StartNodeId, + EndNodeId = e.EndNodeId, + TrajectoryDegree = e.TrajectoryDegree, + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + }); + List nodesDto = [..trafficSolution.Nodes.Select(n => new NodeDto + { + Id = n.Id, + X = n.X, + Y = n.Y, + Name = n.Name, + Direction = MapCompute.GetNodeDirection(n.Direction), + })]; + var splitPath = PathPlanner.PathSplit([.. nodesDto], [.. edgesDto]); + + if (splitPath.IsSuccess) + { + if (splitPath.Data != null) + { + RefreshPath = [..splitPath.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + } + } + } + + private void HandleCompleteState(TrafficSolution trafficSolution) + { + if (trafficSolution.State == TrafficSolutionState.Complete && RefreshPath.Count > 0) + { + NavigationPath = RefreshPath; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + if (MovePurePursuit is not null) + { + MovePurePursuit.UpdatePath([.. NavigationPath]); + MovePurePursuit.OnNodeIndex = MovePurePursuit.GetOnNode(Visualization.X, Visualization.Y).index; + } + RefreshPath = []; + + double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; + double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; + TargetAngle = MathExtension.CheckLimit(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward, 180, -180); + Action = NavigationAction.Rotate; + } + } + + private void HandleGivewayState(TrafficSolution trafficSolution) + { + if (trafficSolution.GivewayNodes[^1].Id == trafficSolution.ReleaseNode.Id && NavigationGoal?.Id != trafficSolution.ReleaseNode.Id) + { + var splitPath = PathPlanner.PathSplit([..trafficSolution.GivewayNodes.Select(n => new NodeDto + { + Id = n.Id, + X = n.X, + Y = n.Y, + Name = n.Name, + Direction = MapCompute.GetNodeDirection(n.Direction), + })], [.. trafficSolution.GivewayEdges.Select(e => new EdgeDto + { + Id = e.Id, + StartNodeId = e.StartNodeId, + EndNodeId = e.EndNodeId, + TrajectoryDegree = e.TrajectoryDegree, + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + })]); + if (splitPath.IsSuccess && splitPath.Data != null && MovePurePursuit != null) + { + NavigationPath = [..splitPath.Data.Select(n => new NavigationNode() + { + Id = n.Id, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + Actions = n.Actions, + })]; + CheckingNodes = [.. NavigationPath.Where(n => n.Actions == "CheckingNode")]; + MovePurePursuit.UpdatePath([.. NavigationPath]); + MovePurePursuit.OnNodeIndex = 0; + + double angleFoward = Math.Atan2(NavigationPath[1].Y - NavigationPath[0].Y, NavigationPath[1].X - NavigationPath[0].X) * 180 / Math.PI; + double angleBacward = Math.Atan2(NavigationPath[0].Y - NavigationPath[1].Y, NavigationPath[0].X - NavigationPath[1].X) * 180 / Math.PI; + TargetAngle = MathExtension.CheckLimit(CheckingNodes[0].Direction == RobotDirection.FORWARD ? angleFoward : angleBacward, 180, -180); + Action = NavigationAction.Rotate; + } + } + } + + private async Task RequestInACS(Dictionary newZones) + { + Guid trafficACSrelaseNodeId = Guid.Empty; + foreach (var zone in newZones) + { + if (zone.Value.Length == 0) trafficACSrelaseNodeId = zone.Key; + else + { + bool requestSuccess = true; + foreach (var zoneACS in zone.Value) + { + if (string.IsNullOrEmpty(zoneACS.Name)) continue; + if (CurrentZones.Any(z => z.Id == zoneACS.Id)) continue; + + var getInTrafficACS = await TrafficACS.RequestIn(RobotModel.RobotId, zoneACS.Name); + if (getInTrafficACS.IsSuccess && getInTrafficACS.Data) CurrentZones.Add(zoneACS); + else + { + requestSuccess = false; + break; + } + } + if (requestSuccess) trafficACSrelaseNodeId = zone.Key; + else break; + } + } + return trafficACSrelaseNodeId; + } + + private void UpdateTraffic() + { + var trafficManagerGoalIndex = CheckingNodes.FindIndex(n => n.Id == TrafficManagerGoalId); + var trafficACSGoalIndex = CheckingNodes.FindIndex(n => n.Id == TrafficACSGoalId); + if (trafficManagerGoalIndex != -1 && trafficACSGoalIndex != -1) + { + var goalIndex = Math.Min(trafficManagerGoalIndex, trafficACSGoalIndex); + NavigationNode goal = CheckingNodes[goalIndex]; + var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == (InNode is null ? Guid.Empty : InNode.Id)); + inNodeIndex = inNodeIndex != -1 ? inNodeIndex : 0; + if (BaseNodes.Count == 0 || BaseNodes[^1].Id != goal.Id) BaseNodes = CheckingNodes.GetRange(inNodeIndex, goalIndex + 1 - inNodeIndex); + + if (IsWaittingStop) + { + if (NavigationState == NavigationStateType.Waitting) + { + TargetAngle = MathExtension.CheckLimit(RotateAngle, 180, -180); + Action = NavigationAction.Rotate; + IsWaittingStop = false; + } + } + else + { + for (int i = inNodeIndex + 1; i <= goalIndex; i++) + { + if (Math.Abs(CheckingNodes[i].Theta - CheckingNodes[i - 1].Theta) > 30) + { + goal = CheckingNodes[i]; + IsWaittingStop = true; + RotateAngle = CheckingNodes[i].Theta; + break; + } + } + UpdateGoal(goal); + } + } + } + + private async Task GoOutTrafficACS() + { + if (BaseNodes is null || BaseNodes.Count == 0 || InNode is null) return; + var goalIndex = CheckingNodes.FindIndex(n => n.Id == BaseNodes[^1].Id); + if (goalIndex == -1) return; + var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == InNode.Id); + inNodeIndex = inNodeIndex != -1 ? inNodeIndex : 0; + if (goalIndex <= inNodeIndex) return; + var baseNodes = CheckingNodes.GetRange(inNodeIndex, goalIndex + 1 - inNodeIndex); + var baseZones = PathPlanner.GetZones([..baseNodes.Select(n => n.ToNodeDto())], Zones); + var outZones = CurrentZones.Where(z => !baseZones.Any(bz => bz.Id == z.Id)).ToList(); + foreach (var zoneACS in outZones) + { + if (string.IsNullOrEmpty(zoneACS.Name)) continue; + var outTrafficACS = await TrafficACS.RequestOut(RobotModel.RobotId, zoneACS.Name); + if (outTrafficACS.IsSuccess && outTrafficACS.Data) CurrentZones.RemoveAll(z => z.Id == zoneACS.Id); + } + } + + protected virtual async Task CheckingHandler() + { + try + { + if (NavigationPath is null || InNode is null) throw new Exception("Đường đi không tồn tại"); + + await GoOutTrafficACS(); + + if (!TrafficManager.Enable) + { + if (TrafficManagerGoalId != CheckingNodes[^1].Id) TrafficManagerGoalId = CheckingNodes[^1].Id; + } + else + { + TrafficManager.UpdateInNode(RobotModel.RobotId, InNode.Id); + var trafficSolution = TrafficManager.GetTrafficNode(RobotModel.RobotId); + if (trafficSolution.State != TrafficSolutionState.RefreshPath && TrafficSolutionState == TrafficSolutionState.RefreshPath && trafficSolution.Edges.Length > 0 && trafficSolution.Nodes.Length > 1) + { + HandleRefreshPath(trafficSolution); + } + if (trafficSolution.State == TrafficSolutionState.Complete || trafficSolution.State == TrafficSolutionState.Waitting) + { + HandleCompleteState(trafficSolution); + } + else if (trafficSolution.State == TrafficSolutionState.GiveWay && trafficSolution.GivewayNodes.Count > 1 && trafficSolution.GivewayEdges.Count > 0) + { + HandleGivewayState(trafficSolution); + } + var goal = CheckingNodes.FirstOrDefault(n => n.Id == trafficSolution.ReleaseNode.Id); + if (goal is not null && goal.Id != TrafficManagerGoalId) TrafficManagerGoalId = goal.Id; + TrafficSolutionState = trafficSolution.State; + } + + if (!TrafficACS.Enable) + { + if (TrafficACSGoalId != CheckingNodes[^1].Id) TrafficACSGoalId = CheckingNodes[^1].Id; + } + else + { + var newZones = TrafficACS.GetZones(InNode?.Id ?? Guid.Empty, [.. CheckingNodes.Select(n => n.ToNodeDto())], Zones); + var trafficACSrelaseNodeId = await RequestInACS(newZones); + if (trafficACSrelaseNodeId != Guid.Empty && trafficACSrelaseNodeId != TrafficACSGoalId) TrafficACSGoalId = trafficACSrelaseNodeId; + } + UpdateTraffic(); + } + catch (Exception ex) + { + Logger.Warning($"{RobotModel.RobotId} Checking ex: {ex.Message}"); + } + } + + private NavigationPathEdge[] GetBasePath() + { + if (InNode is not null && BaseNodes is not null && BaseNodes.Count > 1) + { + var inNodeIndex = BaseNodes.FindIndex(n => n.Id == InNode.Id); + if (inNodeIndex != -1 && inNodeIndex < BaseNodes.Count - 1) + { + var nodes = BaseNodes.GetRange(inNodeIndex, BaseNodes.Count - inNodeIndex); + var edges = MapManager.GetEdges(MapId, [..nodes.Select(n => new NodeDto + { + Id = n.Id, + })]); + if (edges.Length == nodes.Count - 1 && nodes.Count > 1) + { + List pathEdges = []; + var edge = MapManager.GetEdge(nodes[0].Id, nodes[1].Id, MapId); + if (edge is null) + { + pathEdges.Add(new() + { + StartX = Visualization.X, + StartY = Visualization.Y, + EndX = nodes[1].X, + EndY = nodes[1].Y, + Degree = 1, + }); + } + else + { + var splitStartPath = PathPlanner.PathSplit([new NodeDto + { + Id = nodes[0].Id, + X = nodes[0].X, + Y = nodes[0].Y, + }, new NodeDto + { + Id = nodes[1].Id, + X = nodes[1].X, + Y = nodes[1].Y, + Direction = MapCompute.GetNodeDirection(nodes[0].Direction), + }], [edge]); + if (splitStartPath.IsSuccess && splitStartPath.Data is not null) + { + int index = 0; + double minDistance = double.MaxValue; + for (int i = 0; i < splitStartPath.Data.Length; i++) + { + var distance = Math.Sqrt(Math.Pow(Visualization.X - splitStartPath.Data[i].X, 2) + Math.Pow(Visualization.Y - splitStartPath.Data[i].Y, 2)); + if (distance < minDistance) + { + minDistance = distance; + index = i; + } + } + for (int i = index; i < splitStartPath.Data.Length - 1; i++) + { + pathEdges.Add(new() + { + StartX = splitStartPath.Data[i].X, + StartY = splitStartPath.Data[i].Y, + EndX = splitStartPath.Data[i + 1].X, + EndY = splitStartPath.Data[i + 1].Y, + Degree = 1, + }); + } + } + } + for (int i = 1; i < nodes.Count - 1; i++) + { + pathEdges.Add(new() + { + StartX = nodes[i].X, + StartY = nodes[i].Y, + EndX = nodes[i + 1].X, + EndY = nodes[i + 1].Y, + ControlPoint1X = edges[i].ControlPoint1X, + ControlPoint1Y = edges[i].ControlPoint1Y, + ControlPoint2X = edges[i].ControlPoint2X, + ControlPoint2Y = edges[i].ControlPoint2Y, + Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, + }); + } + return [.. pathEdges]; + } + } + } + return []; + } + + private NavigationPathEdge[] GetFullPath() + { + if (InNode is not null && CheckingNodes is not null && CheckingNodes.Count > 1) + { + var inNodeIndex = CheckingNodes.FindIndex(n => n.Id == InNode.Id); + if (inNodeIndex != -1 && inNodeIndex < CheckingNodes.Count - 1) + { + var nodes = CheckingNodes.GetRange(inNodeIndex, CheckingNodes.Count - inNodeIndex); + var edges = MapManager.GetEdges(MapId, [..nodes.Select(n => new NodeDto + { + Id = n.Id, + })]); + if (edges.Length == nodes.Count - 1 && nodes.Count > 1) + { + List pathEdges = []; + var edge = MapManager.GetEdge(nodes[0].Id, nodes[1].Id, MapId); + if (edge is null) + { + pathEdges.Add(new() + { + StartX = Visualization.X, + StartY = Visualization.Y, + EndX = nodes[1].X, + EndY = nodes[1].Y, + Degree = 1, + }); + } + else + { + var splitStartPath = PathPlanner.PathSplit([new NodeDto + { + Id = nodes[0].Id, + X = nodes[0].X, + Y = nodes[0].Y, + }, new NodeDto + { + Id = nodes[1].Id, + X = nodes[1].X, + Y = nodes[1].Y, + Direction = MapCompute.GetNodeDirection(nodes[0].Direction), + }], [edge]); + if (splitStartPath.IsSuccess && splitStartPath.Data is not null) + { + int index = 0; + double minDistance = double.MaxValue; + for (int i = 0; i < splitStartPath.Data.Length; i++) + { + var distance = Math.Sqrt(Math.Pow(Visualization.X - splitStartPath.Data[i].X, 2) + Math.Pow(Visualization.Y - splitStartPath.Data[i].Y, 2)); + if (distance < minDistance) + { + minDistance = distance; + index = i; + } + } + for (int i = index; i < splitStartPath.Data.Length - 1; i++) + { + pathEdges.Add(new() + { + StartX = splitStartPath.Data[i].X, + StartY = splitStartPath.Data[i].Y, + EndX = splitStartPath.Data[i + 1].X, + EndY = splitStartPath.Data[i + 1].Y, + Degree = 1, + }); + } + } + } + for (int i = 1; i < nodes.Count - 1; i++) + { + pathEdges.Add(new() + { + StartX = nodes[i].X, + StartY = nodes[i].Y, + EndX = nodes[i + 1].X, + EndY = nodes[i + 1].Y, + ControlPoint1X = edges[i].ControlPoint1X, + ControlPoint1Y = edges[i].ControlPoint1Y, + ControlPoint2X = edges[i].ControlPoint2X, + ControlPoint2Y = edges[i].ControlPoint2Y, + Degree = edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One ? 1 : edges[i].TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two ? 2 : 3, + }); + } + return [.. pathEdges]; + } + } + } + return []; + } + + public bool UpdateGoal(NavigationNode goal) + { + if (NavigationGoal is null || NavigationGoal.Id != goal.Id) + { + if (NavigationPath is not null && NavigationPath.Any(node => node.Id == goal.Id)) + { + NavigationGoal = goal; + MovePurePursuit?.UpdateGoal(goal); + return true; + } + return false; + } + return true; + } + + private void ClearPath() + { + NavigationPath = null; + NavigationGoal = null; + InNode = null; + CheckingNodes.Clear(); + TrafficManager.DeleteAgent(RobotModel.RobotId); + } + + public void Dispose() + { + CheckingStop(); + NavStop(); + ClearPath(); + NavigationState = NavigationStateType.Ready; + VelocityController.Stop(); + IsCompleted = true; + IsProcessing = false; + GC.SuppressFinalize(this); + } + + protected void NavStart() + { + navTimer = new(SampleTimeMilliseconds, NavigationHandler, Logger); + navTimer.Start(); + IsCompleted = false; + IsCanceled = false; + IsError = false; + IsProcessing = true; + } + + protected void NavStop() + { + navTimer?.Dispose(); + } + + protected void CheckingStart() + { + checkingTimer = new(CheckingTimeMilliseconds, CheckingHandler, Logger); + checkingTimer.Start(); + } + + public void CreateComledted() + { + IsCompleted = true; + IsCanceled = false; + IsError = false; + } + + protected void CheckingStop() + { + checkingTimer?.Dispose(); + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/RobotSimulation.cs b/RobotNet.RobotManager/Services/Simulation/RobotSimulation.cs new file mode 100644 index 0000000..62f1982 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/RobotSimulation.cs @@ -0,0 +1,340 @@ +using Microsoft.EntityFrameworkCore; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.RobotManager.Data; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotManager.Services.Robot; +using RobotNet.RobotManager.Services.Simulation.Models; +using RobotNet.RobotManager.Services.Traffic; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.VDA5050.Factsheet; +using RobotNet.RobotShares.VDA5050.FactsheetExtend; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; +using RobotNet.Shares; +using Action = RobotNet.RobotShares.VDA5050.InstantAction.Action; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class RobotSimulation : IRobotController, IDisposable +{ + public string SerialNumber { get; } + public bool IsOnline { get; set; } = true; + public bool IsWorking => NavigationService is not null && NavigationService.IsProcessing; + public string State { get; } = "Simulation"; + public AutoResetEvent RobotUpdated { get; set; } = new(false); + public string[] CurrentZones => NavigationService is null ? [] : [.. NavigationService.CurrentZones.Select(z => z.Name)]; + public StateMsg StateMsg { get => GetState(); set { } } + public VisualizationMsg VisualizationMsg { get => GetVisualization(); set { } } + public FactSheetMsg FactSheetMsg { get; set; } = new(); + public FactsheetExtendMsg FactsheetExtendMsg { get; set; } = new(); + public VisualizationService Visualization { get; set; } + public NavigationPathEdge[] BasePath => NavigationService is null ? [] : NavigationService.BasePath; + public NavigationPathEdge[] FullPath => NavigationService is null ? [] : NavigationService.FullPath; + public RobotOrderDto OrderState => GetOrderState(); + public RobotActionDto[] ActionStates => []; + + private INavigationService? NavigationService; + private readonly IServiceProvider ServiceProvider; + private readonly LoggerController Logger; + private readonly RobotSimulationModel RobotSimulationModel; + private CancellationTokenSource? CancelRandom; + + public RobotSimulation(string robotid, RobotModel model, IServiceProvider serviceProvider) + { + SerialNumber = robotid; + RobotSimulationModel = new RobotSimulationModel() + { + RobotId = robotid, + Acceleration = 2, + Deceleration = 1, + Length = model.Length / 2, + Width = model.Width, + MaxAngularVelocity = 0.3, + MaxVelocity = 1.5, + RadiusWheel = 0.1, + NavigationType = model.NavigationType, + }; + Visualization = new VisualizationService().WithRadiusWheel(RobotSimulationModel.RadiusWheel).WithRadiusRobot(RobotSimulationModel.Width); + ServiceProvider = serviceProvider; + Logger = ServiceProvider.GetRequiredService>(); + } + + public void Initialize(double x, double y, double theta) => Visualization.LocalizationInitialize(x, y, theta); + + public MessageResult Rotate(double angle) + { + if (NavigationService is not null && NavigationService.NavigationState != NavigationStateType.Ready) return new(false, "Robot chưa sẵn sàng thực hiện nhiệm vụ"); + var olderNavigationService = NavigationService; + NavigationService = NavigationManager.GetNavigation(Visualization, RobotSimulationModel, ServiceProvider); + if (NavigationService is not null) + { + NavigationService.CurrentZones = olderNavigationService is null ? [] : olderNavigationService.CurrentZones; + return NavigationService.Rotate(angle); + } + return new(false, "Model chưa được thiết lập"); + } + + public MessageResult MoveStraight(double x, double y) + { + try + { + var scope = ServiceProvider.CreateAsyncScope(); + var PathPlanningService = scope.ServiceProvider.GetRequiredService(); + var headRobotNode = new NodeDto() + { + Id = Guid.NewGuid(), + X = Visualization.X * Math.Acos(Visualization.Theta * Math.PI / 180), + Y = Visualization.Y * Math.Asin(Visualization.Theta * Math.PI / 180), + }; + var currentRobotNode = new NodeDto() + { + Id = Guid.NewGuid(), + X = Visualization.X, + Y = Visualization.Y, + }; + var goalNode = new NodeDto() + { + Id = Guid.NewGuid(), + X = x, + Y = y, + }; + goalNode.Theta = MapEditorHelper.GetAngle(currentRobotNode, headRobotNode, goalNode) > 90 ? Math.Atan2(currentRobotNode.Y - goalNode.Y, currentRobotNode.X - goalNode.X) : Math.Atan2(goalNode.Y - currentRobotNode.Y, goalNode.X - currentRobotNode.X); + currentRobotNode.Theta = goalNode.Theta; + NodeDto[] nodesplanning = [currentRobotNode, goalNode]; + EdgeDto[] edgesplanning = [new() + { + Id= Guid.NewGuid(), + DirectionAllowed = DirectionAllowed.Both, + TrajectoryDegree = TrajectoryDegree.One, + StartNodeId = currentRobotNode.Id, + EndNodeId = goalNode.Id + }]; + var resultSplit = PathPlanningService.PathSplit(nodesplanning, edgesplanning); + if (!resultSplit.IsSuccess) return new(false, resultSplit.Message); + if (resultSplit.Data is null || resultSplit.Data.Length < 1) return new(false, "Không tìm thấy đường dẫn tới đích"); + if (resultSplit.Data.Length < 1) return new(false, "Dữ liệu truyền vào không đúng"); + var navigationPath = resultSplit.Data.Select(n => new NavigationNode() + { + Id = Guid.NewGuid(), + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = n.Direction == Direction.FORWARD ? RobotShares.Enums.RobotDirection.FORWARD : n.Direction == Direction.BACKWARD ? RobotShares.Enums.RobotDirection.BACKWARD : RobotShares.Enums.RobotDirection.NONE, + Actions = n.Actions, + }).ToArray(); + + if (NavigationService is not null && NavigationService.NavigationState != NavigationStateType.Ready) return new(false, "Robot chưa sẵn sàng thực hiện nhiệm vụ"); + var olderNavigationService = NavigationService; + NavigationService = NavigationManager.GetNavigation(Visualization, RobotSimulationModel, ServiceProvider); + if (NavigationService is not null) + { + NavigationService.CurrentZones = olderNavigationService is null ? [] : olderNavigationService.CurrentZones; + var moveTask = NavigationService.MoveStraight(navigationPath); + return moveTask; + } + return new(false, "Model chưa được thiết lập"); + } + catch (Exception ex) + { + return new(false, ex.Message); + } + } + + public Task CancelOrder() + { + NavigationService?.Cancel(); + CancelRandom?.Cancel(); + return Task.FromResult(new(true)); + } + + public async Task MoveToNode(string goalName, IDictionary>? actions = null, double? lastAngle = null) + { + try + { + var scope = ServiceProvider.CreateAsyncScope(); + var PathPlanner = scope.ServiceProvider.GetRequiredService(); + var AppDb = scope.ServiceProvider.GetRequiredService(); + var TrafficManager = ServiceProvider.GetRequiredService(); + var robotDb = await AppDb.Robots.FirstOrDefaultAsync(r => r.RobotId == SerialNumber); + if (robotDb is null) return new(false, "RobotId không tồn tại"); + var robotModelDb = await AppDb.RobotModels.FirstOrDefaultAsync(r => r.Id == robotDb.ModelId); + if (robotModelDb is null) return new(false, "Robot Model không tồn tại"); + + var path = await PathPlanner.Planning(Visualization.X, Visualization.Y, Visualization.Theta, RobotSimulationModel.NavigationType, robotDb.MapId, goalName); + if (!path.IsSuccess) return new(false, path.Message); + if (path.IsSuccess && path.Data.Nodes.Length < 2) + { + NavigationService?.CreateComledted(); + return new(true, ""); + } + if (path.Data.Nodes is null || path.Data.Edges is null) return new(false, $"Đường dẫn tới đích {goalName} từ [{Visualization.X} - {Visualization.Y} - {Visualization.Theta}] không tồn tại"); + + if (NavigationService is not null && NavigationService.NavigationState != NavigationStateType.Ready) return new(false, "Robot chưa sẵn sàng thực hiện nhiệm vụ"); + var olderNavigationService = NavigationService; + NavigationService = NavigationManager.GetNavigation(Visualization, RobotSimulationModel, ServiceProvider); + if (NavigationService is not null) + { + var nodes = path.Data.Nodes; + var createAgent = TrafficManager.CreateAgent(robotDb.MapId, this, new() + { + NavigationType = robotModelDb.NavigationType, + Length = robotModelDb.Length, + Width = robotModelDb.Width, + NavigationPointX = robotModelDb.OriginX, + NavigationPointY = robotModelDb.OriginY, + }, + [..nodes.Select(n => new TrafficNodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Theta = n.Theta, + Direction = MapCompute.GetRobotDirection(n.Direction), + })], [..path.Data.Edges.Select(n => new TrafficEdgeDto() + { + Id = n.Id, + StartNodeId = n.StartNodeId, + EndNodeId = n.EndNodeId, + TrajectoryDegree = n.TrajectoryDegree, + ControlPoint1X = n.ControlPoint1X, + ControlPoint1Y = n.ControlPoint1Y, + ControlPoint2X = n.ControlPoint2X, + ControlPoint2Y = n.ControlPoint2Y, + })]); + if (!createAgent.IsSuccess) + { + Logger.Warning($"{SerialNumber} - Không thể tạo traffic agent: {createAgent.Message}"); + return new(false, $"Không thể tạo traffic agent: {createAgent.Message}"); + } + NavigationService.MapId = robotDb.MapId; + NavigationService.CurrentZones = olderNavigationService is null ? [] : olderNavigationService.CurrentZones; + var moveTask = NavigationService.Move(nodes, path.Data.Edges); + return moveTask; + } + return new(false, "Model chưa được thiết lập"); + } + catch (Exception ex) + { + return new(false, ex.Message); + } + } + + private VisualizationMsg GetVisualization() + { + var scope = ServiceProvider.CreateAsyncScope(); + var AppDb = scope.ServiceProvider.GetRequiredService(); + var robotDb = AppDb.Robots.FirstOrDefault(r => r.RobotId == SerialNumber); + return new() + { + SerialNumber = SerialNumber, + MapId = robotDb is null ? string.Empty : robotDb.MapId.ToString(), + AgvPosition = new() + { + X = Visualization.X, + Y = Visualization.Y, + Theta = Visualization.Theta, + PositionInitialized = true, + LocalizationScore = 100, + DeviationRange = 100, + }, + Velocity = new() + { + Vx = Visualization.Vx, + Vy = Visualization.Vy, + Omega = Visualization.Omega, + } + }; + } + + private StateMsg GetState() + { + return new() + { + SerialNumber = SerialNumber, + Timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + NewBaseRequest = true, + BatteryState = new() + { + BatteryHealth = 100, + BatteryCharge = 0, + BatteryVoltage = 24, + Charging = false, + }, + ActionStates = [], + Errors = [], + Information = [], + NodeStates = [], + Loads = [], + }; + } + + public Task> InstantAction(Action action, bool waittingFeedback) + { + return Task.FromResult>(new(true)); + } + + public void Log(string message, LogLevel level = LogLevel.Information) + { + switch (level) + { + case LogLevel.Trace: Logger.Trace($"{SerialNumber} - {message}"); break; + case LogLevel.Error: Logger.Error($"{SerialNumber} - {message}"); break; + case LogLevel.Warning: Logger.Warning($"{SerialNumber} - {message}"); break; + case LogLevel.Critical: Logger.Critical($"{SerialNumber} - {message}"); break; + case LogLevel.Information: Logger.Info($"{SerialNumber} - {message}"); break; + case LogLevel.Debug: Logger.Debug($"{SerialNumber} - {message}"); break; + default: Logger.Debug($"{SerialNumber} - {message}"); break; + } + } + + public Task MoveRandom(List nodes) + { + try + { + CancelRandom = new CancellationTokenSource(); + var random = new Random(); + var randomTask = Task.Run(async () => + { + while (!CancelRandom.IsCancellationRequested) + { + var index = random.Next(nodes.Count); + await MoveToNode(nodes[index]); + while (!CancelRandom.IsCancellationRequested) + { + if (!IsWorking) break; + await Task.Delay(1000); + } + await Task.Delay(2000); + } + }, cancellationToken: CancelRandom.Token); + } + catch { } + return Task.FromResult(new(true)); + } + + public Task CancelAction() + { + return Task.FromResult(new(true, "")); + } + + public RobotOrderDto GetOrderState() + { + return new() + { + OrderId = StateMsg.OrderId, + IsCompleted = NavigationService is not null && NavigationService.IsCompleted, + IsError = NavigationService is not null && NavigationService.IsError, + IsProcessing = NavigationService is not null && NavigationService.IsProcessing, + IsCanceled = NavigationService is not null && NavigationService.IsCanceled, + Errors = NavigationService is not null ? NavigationService.Errors : [], + }; + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/VelocityController.cs b/RobotNet.RobotManager/Services/Simulation/VelocityController.cs new file mode 100644 index 0000000..e18ace8 --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/VelocityController.cs @@ -0,0 +1,75 @@ +namespace RobotNet.RobotManager.Services.Simulation; + +public class VelocityController(VisualizationService Visualization) +{ + private double Acceleration = 2; + private double Deceleration = 10; + private double AngularVelLeft; + private double AngularVelRight; + private double SampleTime = 0.05; + + public VelocityController WithAcceleration(double acc) + { + Acceleration = acc; + return this; + } + + public VelocityController WithDeceleration(double dec) + { + Deceleration = dec; + return this; + } + + public VelocityController WithSampleTime(double time) + { + SampleTime = time; + return this; + } + + private (double angularVelLeft, double angularVelRight) AccelerationCalculator(double wL, double wR, double wL_Current, double wR_Current) + { + var angularVelLeft = wL_Current; + var angularVelRight = wR_Current; + if (wL_Current == 0 || wL / wL_Current < 0) + { + if (wL != 0) angularVelLeft += wL / Math.Abs(wL) * Acceleration; + else angularVelLeft = wL; + } + else + { + if (Math.Abs(wL) - Math.Abs(wL_Current) > Acceleration) angularVelLeft += Acceleration * wL_Current / Math.Abs(wL_Current); + else if (Math.Abs(wL_Current) - Math.Abs(wL) > Deceleration) angularVelLeft -= Deceleration * wL_Current / Math.Abs(wL_Current); + else angularVelLeft = wL; + } + + if (wR_Current == 0 || wR / wR_Current < 0) + { + if (wR != 0) angularVelRight += wR / Math.Abs(wR) * Acceleration; + else angularVelRight = wR; + } + else + { + if (Math.Abs(wR) - Math.Abs(wR_Current) > Acceleration) angularVelRight += Acceleration * wR_Current / Math.Abs(wR_Current); + else if (Math.Abs(wR_Current) - Math.Abs(wR) > Deceleration) angularVelRight -= Deceleration * wR_Current / Math.Abs(wR_Current); + else angularVelRight = wR; + } + + if (Math.Abs(angularVelLeft) > Math.Abs(wL)) angularVelLeft = wL; + if (Math.Abs(angularVelRight) > Math.Abs(wR)) angularVelRight = wR; + return (angularVelLeft, angularVelRight); + } + + public bool SetSpeed(double angularVelLeft, double angularVelRight) + { + (AngularVelLeft, AngularVelRight) = AccelerationCalculator(angularVelLeft, angularVelRight, AngularVelLeft, AngularVelRight); + //Console.WriteLine($"AngularVelLeft = {AngularVelLeft:0.####}, AngularVelRight = {AngularVelRight:0.####}"); + _ = Visualization.UpdatePosition(AngularVelLeft, AngularVelRight, SampleTime); + return true; + } + + public void Stop() + { + (AngularVelLeft, AngularVelRight) = (0, 0); + _ = Visualization.UpdatePosition(AngularVelLeft, AngularVelRight, SampleTime); + } +} diff --git a/RobotNet.RobotManager/Services/Simulation/VisualizationService.cs b/RobotNet.RobotManager/Services/Simulation/VisualizationService.cs new file mode 100644 index 0000000..d5c1abe --- /dev/null +++ b/RobotNet.RobotManager/Services/Simulation/VisualizationService.cs @@ -0,0 +1,54 @@ +using RobotNet.RobotManager.Services.Simulation.Algorithm; +using RobotNet.RobotManager.Services.Simulation.Models; + +namespace RobotNet.RobotManager.Services.Simulation; + +public class VisualizationService +{ + public double X { get; private set; } + public double Y { get; private set; } + public double Theta { get; private set; } + public double Vx { get; private set; } + public double Vy { get; private set; } + public double Omega { get; private set; } + + private double RadiusWheel; + private double RadiusRobot; + + public VisualizationService WithRadiusWheel(double radiusWheel) + { + RadiusWheel = radiusWheel; + return this; + } + + public VisualizationService WithRadiusRobot(double radiusRobot) + { + RadiusRobot = radiusRobot; + return this; + } + + public (double x, double y, double angle) UpdatePosition(double wL, double wR, double time) + { + Theta = (Theta + time * (-wR - wL) * RadiusWheel / RadiusRobot * MathExtension.ToDegree) % 360; + X += time * (-wR + wL) * RadiusWheel * Math.Cos(Theta * MathExtension.ToRad) / 2; + Y += time * (-wR + wL) * RadiusWheel * Math.Sin(Theta * MathExtension.ToRad) / 2; + _ = UpdateVelocity(wL, wR); + if (Theta > 180) Theta -= 360; + else if (Theta < -180) Theta += 360; + return (X, Y, Theta); + } + + public (double vx, double vy, double omega) UpdateVelocity(double wL, double wR) + { + Vx = (-wR + wL) * RadiusWheel / 2; + Omega = (-wR - wL) * RadiusWheel / RadiusRobot; + return (Vx, 0, Omega); + } + + public void LocalizationInitialize(double x, double y, double theta) + { + X = x; + Y = y; + Theta = theta; + } +} diff --git a/RobotNet.RobotManager/Services/Traffic/Agent.cs b/RobotNet.RobotManager/Services/Traffic/Agent.cs new file mode 100644 index 0000000..9bf9e6d --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/Agent.cs @@ -0,0 +1,269 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotManager.Services.Planner.Space; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Traffic; + + +public class Agent +{ + public IRobotController Robot { get; set; } = null!; + public AgentModel AgentModel { get; set; } = new(); + public Guid MapId { get; set; } + public int InNodeIndex { get; set; } + public TrafficNodeDto InNode { get; set; } = new(); + public int ReleaseNodeIndex => Nodes.IndexOf(ReleaseNode); + public TrafficNodeDto ReleaseNode { get; set; } = new(); + public TrafficNodeDto GoalNode { get; set; } = new(); + public List Nodes { get; set; } = []; + public List Edges { get; set; } = []; + public List SubNodes { get; set; } = []; + public List SubEdges { get; set; } = []; + public List GivewayNodes { get; set; } = []; + public List GivewayEdges { get; set; } = []; + public TrafficSolutionState State { get; set; } + public RefreshPathState RefreshPathState { get; set; } = RefreshPathState.Compeleted; + public string Message { get; set; } = ""; + + private static double GetDistance(List nodes) + { + double distance = 0; + for (int i = 0; i < nodes.Count - 1; i++) + { + distance += Math.Sqrt(Math.Pow(nodes[i].X - nodes[i + 1].X, 2) + Math.Pow(nodes[i].Y - nodes[i + 1].Y, 2)); + } + return distance; + } + + public bool Checking(double trafficAvoidableNodeMax, double trafficDistanceMax) + { + if (State != TrafficSolutionState.GiveWay && State != TrafficSolutionState.LoopResolve && + RefreshPathState != RefreshPathState.Created && RefreshPathState != RefreshPathState.Refreshing) + { + if (ReleaseNodeIndex != -1 && InNodeIndex <= ReleaseNodeIndex && InNodeIndex < Nodes.Count) + { + var releaseNodes = Nodes.GetRange(InNodeIndex + 1, ReleaseNodeIndex - InNodeIndex); + if (releaseNodes.Where(n => n.IsAvoidableNode).Count() < trafficAvoidableNodeMax) return true; + var distance = GetDistance([.. Nodes.GetRange(InNodeIndex, ReleaseNodeIndex - InNodeIndex + 1)]); + distance -= Math.Sqrt(Math.Pow(Robot.VisualizationMsg.AgvPosition.X - Nodes[InNodeIndex].X, 2) + Math.Pow(Robot.VisualizationMsg.AgvPosition.Y - Nodes[InNodeIndex].Y, 2)); + if (distance < trafficDistanceMax) return true; + } + } + return false; + } + + public TrafficNodeDto[] GetChekingNodes(double trafficAvoidableNodeMax, double trafficDistanceMin) + { + List releaseNodes = []; + double distance = 0; + distance -= Math.Sqrt(Math.Pow(Nodes[InNodeIndex].X - Robot.VisualizationMsg.AgvPosition.X, 2) + Math.Pow(Nodes[InNodeIndex].Y - Robot.VisualizationMsg.AgvPosition.Y, 2)); + int index = InNodeIndex < ReleaseNodeIndex ? InNodeIndex + 1 : InNodeIndex; + for (; index < Nodes.Count; index++) + { + releaseNodes.Add(Nodes[index]); + if (index < Nodes.Count - 1) + { + var remainingNodes = Nodes.GetRange(index + 1, Nodes.Count - (index + 1)); + if (!remainingNodes.Any(n => n.IsAvoidableNode)) + { + index = Nodes.Count; + break; + } + } + if (index > 0) distance += Math.Sqrt(Math.Pow(Nodes[index].X - Nodes[index - 1].X, 2) + Math.Pow(Nodes[index].Y - Nodes[index - 1].Y, 2)); + if (distance < trafficDistanceMin || !Nodes[index].IsAvoidableNode) continue; + if (releaseNodes.Where(n => n.IsAvoidableNode).Count() >= trafficAvoidableNodeMax) break; + } + return index > ReleaseNodeIndex ? [.. Nodes.GetRange(ReleaseNodeIndex, index - ReleaseNodeIndex + (index < Nodes.Count ? 1 : 0))] : []; + } + + public (TrafficSolutionState state, string message) UpdateGiveWay(TrafficNodeDto[] giveWayNodes, PathPlanner pathPlanner, MapManager map) + { + try + { + if (giveWayNodes.Length < 2) return (TrafficSolutionState.UnableResolve, "Lộ trình tránh đường không hợp lệ"); + if (!giveWayNodes.Any(n => !Nodes.Contains(n))) + { + var giveNodeIndex = Nodes.IndexOf(giveWayNodes[^1]); + if (giveNodeIndex >= ReleaseNodeIndex) + { + ReleaseNode = giveWayNodes[^1]; + State = TrafficSolutionState.Complete; + return (TrafficSolutionState.Complete, ""); + } + } + var giveWayIndex = Nodes.IndexOf(giveWayNodes[0]); + var releaseNodeInGivewaysIndex = Array.FindIndex(giveWayNodes, n=> n.Id == ReleaseNode.Id); + if (giveWayIndex != -1) + { + List subGiveWayNodes = []; + List subGiveWayEdges = []; + if (releaseNodeInGivewaysIndex != -1) subGiveWayNodes = giveWayNodes.ToList().GetRange(releaseNodeInGivewaysIndex, giveWayNodes.Length - releaseNodeInGivewaysIndex); + else if (giveWayIndex == ReleaseNodeIndex) subGiveWayNodes = [.. giveWayNodes]; + else if (giveWayIndex < ReleaseNodeIndex) + { + subGiveWayNodes = Nodes.GetRange(giveWayIndex + 1, ReleaseNodeIndex - giveWayIndex); + subGiveWayNodes.Reverse(); + subGiveWayNodes.AddRange(giveWayNodes); + } + else + { + subGiveWayNodes = Nodes.GetRange(ReleaseNodeIndex, giveWayIndex - ReleaseNodeIndex); + subGiveWayNodes.AddRange(giveWayNodes); + } + List edges = [..map.GetEdges(MapId, [..subGiveWayNodes.Select(n => new NodeDto + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + MapId = MapId, + })])]; + if (edges.Count > 0) + { + double releaseTheta; + if (ReleaseNodeIndex > 0) + { + var (nearNodeX, nearNodeY) = MapEditorHelper.Curve(0.9, new EdgeCaculatorModel + { + TrajectoryDegree = Edges[ReleaseNodeIndex - 1].TrajectoryDegree, + ControlPoint1X = Edges[ReleaseNodeIndex - 1].ControlPoint1X, + ControlPoint1Y = Edges[ReleaseNodeIndex - 1].ControlPoint1Y, + ControlPoint2X = Edges[ReleaseNodeIndex - 1].ControlPoint2X, + ControlPoint2Y = Edges[ReleaseNodeIndex - 1].ControlPoint2Y, + X1 = Nodes[ReleaseNodeIndex - 1].X, + Y1 = Nodes[ReleaseNodeIndex - 1].Y, + X2 = Nodes[ReleaseNodeIndex].X, + Y2 = Nodes[ReleaseNodeIndex].Y, + }); + var relaseForward = Math.Atan2(Nodes[ReleaseNodeIndex].Y - nearNodeY, Nodes[ReleaseNodeIndex].X - nearNodeX) * 180 / Math.PI; + var releaseBackward = Math.Atan2(nearNodeY - Nodes[ReleaseNodeIndex].Y, nearNodeX - Nodes[ReleaseNodeIndex].X) * 180 / Math.PI; + releaseTheta = Nodes[ReleaseNodeIndex - 1].Direction == RobotDirection.FORWARD ? relaseForward : releaseBackward; + } + else + { + var (nearNodeX, nearNodeY) = MapEditorHelper.Curve(0.1, new EdgeCaculatorModel + { + TrajectoryDegree = Edges[ReleaseNodeIndex].TrajectoryDegree, + ControlPoint1X = Edges[ReleaseNodeIndex].ControlPoint1X, + ControlPoint1Y = Edges[ReleaseNodeIndex].ControlPoint1Y, + ControlPoint2X = Edges[ReleaseNodeIndex].ControlPoint2X, + ControlPoint2Y = Edges[ReleaseNodeIndex].ControlPoint2Y, + X1 = Nodes[ReleaseNodeIndex].X, + Y1 = Nodes[ReleaseNodeIndex].Y, + X2 = Nodes[ReleaseNodeIndex + 1].X, + Y2 = Nodes[ReleaseNodeIndex + 1].Y, + }); + var releaseBackward = Math.Atan2(Nodes[ReleaseNodeIndex].Y - nearNodeY, Nodes[ReleaseNodeIndex].X - nearNodeX) * 180 / Math.PI; + var relaseForward = Math.Atan2(nearNodeY - Nodes[ReleaseNodeIndex].Y, nearNodeX - Nodes[ReleaseNodeIndex].X) * 180 / Math.PI; + releaseTheta = Nodes[0].Direction == RobotDirection.FORWARD ? relaseForward : releaseBackward; + } + List nodes = [..PathPlanner.CalculatorDirection(releaseTheta, [..subGiveWayNodes.Select(n => new NodeDto + { + Id = n.Id, + X = n.X, + Y = n.Y, + Name = n.Name, + MapId = MapId, + })], [..edges])]; + GivewayNodes = [.. nodes.Select(n => new TrafficNodeDto + { + Id = n.Id, + X = n.X, + Y = n.Y, + Name = n.Name, + Direction = MapCompute.GetRobotDirection(n.Direction), + })]; + //Console.WriteLine($"{Robot.SerialNumber} giveway: [{string.Join(",", GivewayNodes.Select(n => $"({n.Name} - {n.Direction})"))}]"); + foreach (var node in GivewayNodes) + { + node.IsAvoidableNode = map.GetNegativeNodes(MapId, node.Id).Length > 2; + if (node.IsAvoidableNode) + { + node.AvoidablePaths = [.. map.GetNegativePaths(MapId, new() { Id = node.Id, X = node.X, Y = node.Y, Name = node.Name }, 2) + .Where(path => path.Length > 0) + .Select(path => path.Select(n => new TrafficNodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name }).ToArray())]; + } + } + GivewayEdges = [.. edges.Select(n => new TrafficEdgeDto + { + Id = n.Id, + ControlPoint1X = n.ControlPoint1X, + ControlPoint1Y = n.ControlPoint1Y, + ControlPoint2X = n.ControlPoint2X, + ControlPoint2Y = n.ControlPoint2Y, + StartNodeId = n.StartNodeId, + EndNodeId = n.EndNodeId, + TrajectoryDegree = n.TrajectoryDegree, + })]; + State = TrafficSolutionState.GiveWay; + var angleForward = Math.Atan2(GivewayNodes[^1].Y - GivewayNodes[^2].Y, GivewayNodes[^1].X - GivewayNodes[^2].X) * 180 / Math.PI; + var angleBackward = Math.Atan2(GivewayNodes[^2].Y - GivewayNodes[^1].Y, GivewayNodes[^2].X - GivewayNodes[^1].X) * 180 / Math.PI; + RefreshPath(GivewayNodes[^1], GivewayNodes[^1].Direction == RobotDirection.FORWARD ? angleForward : angleBackward, pathPlanner, map); + return (TrafficSolutionState.GiveWay, ""); + } + return (TrafficSolutionState.UnableResolve, $"Không tìm thấy edge yêu cầu phải tránh [{GivewayNodes[0].Name} - {GivewayNodes[1].Name}]"); + } + return (TrafficSolutionState.UnableResolve, $"Không tìm thấy điểm xung đột {GivewayNodes[0].Name}"); + } + catch (Exception ex) + { + return (TrafficSolutionState.UnableResolve, $"Cập nhật lộ trình tránh đường xảy ra lỗi: {ex.Message}"); + } + } + + private void RefreshPath(TrafficNodeDto currentNode, double theta, PathPlanner planner, MapManager map) + { + RefreshPathState = RefreshPathState.Created; + var plannerTask = Task.Run(async () => + { + RefreshPathState = RefreshPathState.Refreshing; + var path = await planner.Planning(currentNode.Id, theta, AgentModel.NavigationType, MapId, GoalNode.Id); + if (!path.IsSuccess) + { + RefreshPathState = RefreshPathState.Error; + Message = path.Message; + return; + } + if (path.Data.Nodes is null || path.Data.Edges is null || path.Data.Edges.Length == 0 || path.Data.Nodes.Length < 2) + { + RefreshPathState = RefreshPathState.Error; + Message = $"Đường dẫn tới đích [{GoalNode.Name} - {GoalNode.Id}] từ [{currentNode.Name} - {currentNode.Id}] không tồn tại"; + return; + } + SubEdges = [..path.Data.Edges.Select(n => new TrafficEdgeDto() + { + Id = n.Id, + StartNodeId = n.StartNodeId, + EndNodeId = n.EndNodeId, + TrajectoryDegree = n.TrajectoryDegree, + ControlPoint1X = n.ControlPoint1X, + ControlPoint1Y = n.ControlPoint1Y, + ControlPoint2X = n.ControlPoint2X, + ControlPoint2Y = n.ControlPoint2Y, + })]; + SubNodes = [..path.Data.Nodes.Select(n => new TrafficNodeDto() + { + Id = n.Id, + Name = n.Name, + X = n.X, + Y = n.Y, + Direction = MapCompute.GetRobotDirection(n.Direction) + })]; + foreach (var node in SubNodes) + { + node.IsAvoidableNode = map.GetNegativeNodes(MapId, node.Id).Length > 2; + if (node.IsAvoidableNode) + { + node.AvoidablePaths = [.. map.GetNegativePaths(MapId, new() { Id = node.Id, X = node.X, Y = node.Y, Name = node.Name }, 2) + .Where(path => path.Length > 0) + .Select(path => path.Select(n => new TrafficNodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name }).ToArray())]; + } + } + //Console.WriteLine($"{Robot.SerialNumber} refresh path: [{string.Join(",", SubNodes.Select(n => $"({n.Name} - {n.Direction})"))}]"); + RefreshPathState = RefreshPathState.Compeleted; + }); + } +} diff --git a/RobotNet.RobotManager/Services/Traffic/AgentModel.cs b/RobotNet.RobotManager/Services/Traffic/AgentModel.cs new file mode 100644 index 0000000..14560b4 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/AgentModel.cs @@ -0,0 +1,13 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class AgentModel +{ + public NavigationType NavigationType { get; set; } + public double Length { get; set; } + public double Width { get; set; } + public double NavigationPointX { get; set; } + public double NavigationPointY { get; set; } + public double TurningRadius { get; set; } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficAlarm.cs b/RobotNet.RobotManager/Services/Traffic/TrafficAlarm.cs new file mode 100644 index 0000000..3331072 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficAlarm.cs @@ -0,0 +1,5 @@ +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficAlarm +{ +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficConflict.cs b/RobotNet.RobotManager/Services/Traffic/TrafficConflict.cs new file mode 100644 index 0000000..950db55 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficConflict.cs @@ -0,0 +1,15 @@ +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Traffic; + +#nullable disable + +public class TrafficConflict +{ + public Agent AgentRequest { get; set; } + public Agent AgentConflict { get; set; } + public TrafficNodeDto NodeConflict { get; set; } + public List ReleaseNodes { get; set; } + public TrafficConflictState State { get; set; } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficEdgeDto.cs b/RobotNet.RobotManager/Services/Traffic/TrafficEdgeDto.cs new file mode 100644 index 0000000..bd9421e --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficEdgeDto.cs @@ -0,0 +1,20 @@ +using RobotNet.MapShares.Enums; +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services.Traffic; + +#nullable disable + +public class TrafficEdgeDto +{ + public Guid Id { get; set; } + public Guid StartNodeId { get; set; } + public Guid EndNodeId { get; set; } + public TrafficNodeDto StartNode { get; set; } + public TrafficNodeDto EndNode { get; set; } + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } + public TrajectoryDegree TrajectoryDegree { get; set; } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficGiveway.cs b/RobotNet.RobotManager/Services/Traffic/TrafficGiveway.cs new file mode 100644 index 0000000..a0cf98c --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficGiveway.cs @@ -0,0 +1,13 @@ +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services.Traffic; + +#nullable disable + +public class TrafficGiveway +{ + public string RobotGive { get; set; } + public string RobotReceive { get; set; } + public TrafficNodeDto[] Nodes { get; set; } + public bool IsGiveway { get; set; } = false; +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficManager.cs b/RobotNet.RobotManager/Services/Traffic/TrafficManager.cs new file mode 100644 index 0000000..5a30bfb --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficManager.cs @@ -0,0 +1,857 @@ +using RobotNet.RobotManager.Data; +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; +using RobotNet.Shares; +using System.Collections.Concurrent; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficManager(MapManager Map, IServiceProvider ServiceProvider, PathPlanner PathPlannger, IConfiguration Configuration, LoggerController Logger) : IHostedService +{ + private System.Threading.Timer? computeTimer; + private const int intervalTime = 1000; + + private readonly double TrafficCheckingDistanceMax = Configuration.GetValue("TrafficConfig:CheckingDistanceMax", 5); + private readonly double TrafficCheckingDistanceMin = Configuration.GetValue("TrafficConfig:CheckingDistanceMin", 3); + private readonly int TrafficResolutionRepeat = Configuration.GetValue("TrafficConfig:ResolutionRepeat", 1); + private readonly int TrafficAvoidableNodeMax = Configuration.GetValue("TrafficConfig:AvoidableNodeMax", 3); + private readonly double TrafficOffsetLength = Configuration.GetValue("TrafficConfig:OffsetLength", 0.3); + private readonly double TrafficOffsetWidth = Configuration.GetValue("TrafficConfig:OffsetWidth", 0.2); + public bool Enable => Configuration.GetValue("TrafficConfig:Enable", false); + + private readonly SemaphoreSlim AgentSemaphore = new(1, 1); + private readonly SemaphoreSlim UpdateSemaphore = new(1, 1); + + public ConcurrentDictionary TrafficMaps { get; set; } = []; + + private Agent NewAgent(Guid mapId, IRobotController robotController, AgentModel model, TrafficNodeDto[] nodes, TrafficEdgeDto[] edges) + { + model.TurningRadius = TrafficMath.CalTurningRadius(model, TrafficOffsetLength, TrafficOffsetWidth); + foreach (var node in nodes) + { + node.AvoidablePaths = [.. Map.GetNegativePaths(mapId, new() { Id = node.Id, X = node.X, Y = node.Y, Name = node.Name }, model.TurningRadius) + .Where(path => path.Length > 0) + .Select(path => path.Select(n => new TrafficNodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name }).ToArray())]; + node.IsAvoidableNode = Map.GetNegativeNodes(mapId, node.Id).Length > 2 && node.AvoidablePaths.Length > 0; + if (nodes.Length > 0 && node != nodes.Last() && model.NavigationType != NavigationType.Forklift) node.LockedShapes = new() + { + Type = TrafficLockedShapeType.Circle, + Radius = model.TurningRadius, + }; + else node.LockedShapes = TrafficMath.CalRobotRectShape(model, TrafficOffsetLength, TrafficOffsetWidth, node.X, node.Y, node.Theta); + } + Agent agent = new() + { + Robot = robotController, + AgentModel = model, + Nodes = [.. nodes], + InNodeIndex = 0, + ReleaseNode = nodes[0], + Edges = [.. edges], + State = TrafficSolutionState.Complete, + MapId = mapId, + GoalNode = nodes[^1], + }; + return agent; + } + + public MessageResult CreateAgent(Guid mapId, IRobotController robotController, AgentModel model, TrafficNodeDto[] nodes, TrafficEdgeDto[] edges) + { + if (AgentSemaphore.Wait(2000)) + { + try + { + if (TrafficMaps.TryGetValue(mapId, out TrafficMap? TrafficMap) && TrafficMap is not null) + { + if (TrafficMap.Agents.TryGetValue(robotController.SerialNumber, out _)) TrafficMap.Agents.Remove(robotController.SerialNumber); + TrafficMap.Agents.Add(robotController.SerialNumber, NewAgent(mapId, robotController, model, nodes, edges)); + } + else + { + bool result = TrafficMaps.TryAdd(mapId, new TrafficMap() + { + MapId = mapId, + Agents = new Dictionary() + { + { + robotController.SerialNumber, + NewAgent(mapId, robotController, model, nodes, edges) + } + }, + }); + + if (!result) return new(false, $"Không thể tạo TrafficMap cho bản đồ {mapId} do đã tồn tại hoặc xảy ra lỗi khi thêm agent {robotController.SerialNumber}."); + } + //Logger.Info($"CreateAgent: Tạo Agent thành công - {robotController.SerialNumber} to {nodes[^1].Name}, nodes: [{string.Join(", ", nodes.Select(n => $"[({n.Name}) - ({n.Direction})]"))}]"); + + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"Tạo mới traffic Agent xảy ra lỗi - {ex.Message}"); + return new(false, $"Tạo mới traffic Agent xảy ra lỗi - {ex.Message}"); + } + finally + { + AgentSemaphore.Release(); + } + } + else return new(false, $"Không thể tạo trafficAgent do vượt quá thời gian chờ."); + } + + public MessageResult DeleteAgent(string robotId) + { + if (AgentSemaphore.Wait(2000)) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var RobotDb = scope.ServiceProvider.GetRequiredService(); + var robot = RobotDb.Robots.FirstOrDefault(r => r.RobotId == robotId); + if (robot is null) return new(false, $"Không tìm thấy robot Id {robotId} trong kho dữ liệu."); + if (TrafficMaps.TryGetValue(robot.MapId, out TrafficMap? TrafficMap) && TrafficMap is not null) + { + if (TrafficMap.Agents.TryGetValue(robotId, out _)) + { + var remove = TrafficMap.Agents.Remove(robotId); + if (!remove) return new(false, $"Không thể xóa agent {robotId}."); + } + } + + //Logger.Info($"DeleteAgent: Xóa Agent thành công - {robotId}"); + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"Xóa traffic Agent xảy ra lỗi - {ex.Message}"); + return new(false, $"Xóa traffic Agent xảy ra lỗi - {ex.Message}"); + } + finally + { + AgentSemaphore.Release(); + } + } + else return new(false, $"Không thể xóa trafficAgent do vượt quá thời gian chờ."); + } + + public TrafficSolution GetTrafficNode(string robotId) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var RobotDb = scope.ServiceProvider.GetRequiredService(); + var robot = RobotDb.Robots.FirstOrDefault(r => r.RobotId == robotId); + if (robot is null) return new() { State = TrafficSolutionState.None }; + + if (TrafficMaps.TryGetValue(robot.MapId, out TrafficMap? trafficmap) && trafficmap is not null) + { + if (trafficmap.Agents.TryGetValue(robotId, out Agent? agent) && agent is not null) + { + TrafficSolution returnSolution = new() + { + State = agent.State, + ReleaseNode = agent.ReleaseNode, + GivewayEdges = agent.GivewayEdges, + GivewayNodes = agent.GivewayNodes, + Nodes = [.. agent.Nodes], + Edges = [.. agent.Edges] + }; ; + switch (agent.State) + { + case TrafficSolutionState.GiveWay: + if (agent.InNodeIndex != agent.ReleaseNodeIndex && (agent.GivewayNodes.Count > 0 && agent.ReleaseNode.Id != agent.GivewayNodes[^1].Id)) returnSolution.State = TrafficSolutionState.Waitting; + else + { + if (agent.GivewayNodes.Count > 0 && agent.ReleaseNode.Id != agent.GivewayNodes[^1].Id) agent.ReleaseNode = agent.GivewayNodes[^1]; + if (agent.GivewayNodes.Count == 0 && agent.RefreshPathState == RefreshPathState.Compeleted) + { + agent.State = TrafficSolutionState.RefreshPath; + returnSolution.State = TrafficSolutionState.RefreshPath; + } + } + break; + case TrafficSolutionState.LoopResolve: returnSolution.State = TrafficSolutionState.Complete; break; + }; + // Console.WriteLine($"{robotId} Gettraffic: {returnSolution.State}, release: {returnSolution.ReleaseNode.Name}"); + return returnSolution; + } + } + return new() { State = TrafficSolutionState.None }; + } + catch (Exception ex) + { + Logger.Warning($"{robotId} Lấy thông tin traffic xảy ra lỗi: {ex.Message}"); + return new() { State = TrafficSolutionState.None }; + } + } + + public MessageResult UpdateInNode(string robotId, Guid nodeId) + { + if (UpdateSemaphore.Wait(2000)) + { + try + { + using var scope = ServiceProvider.CreateScope(); + var RobotDb = scope.ServiceProvider.GetRequiredService(); + var robot = RobotDb.Robots.FirstOrDefault(r => r.RobotId == robotId); + if (robot is null) return new(false, $"Không tìm thấy robot Id {robotId} trong kho dữ liệu."); + + if (TrafficMaps.TryGetValue(robot.MapId, out TrafficMap? trafficmap) && trafficmap is not null) + { + if (trafficmap.Agents.TryGetValue(robotId, out Agent? agent) && agent is not null) + { + if (agent.State == TrafficSolutionState.GiveWay) + { + if (agent.GivewayNodes.Count > 1 && nodeId == agent.GivewayNodes[^1].Id) + { + var giveWayResolution = trafficmap.GivewayResolution.FirstOrDefault(n => n.RobotGive == robotId); + if (giveWayResolution is not null) + { + trafficmap.AddLocker(giveWayResolution.RobotReceive, [.. giveWayResolution.Nodes]); + giveWayResolution.IsGiveway = true; + } + agent.Nodes = agent.SubNodes; + agent.Edges = agent.SubEdges; + agent.GivewayNodes = []; + agent.GivewayEdges = []; + } + + List lockedNodes = []; + var nodeIndex = agent.Nodes.FindIndex(n => n.Id == nodeId); + if (nodeIndex != -1) + { + agent.InNodeIndex = nodeIndex; + if (agent.ReleaseNodeIndex != -1 && agent.InNodeIndex <= agent.ReleaseNodeIndex) + lockedNodes = agent.Nodes.GetRange(agent.InNodeIndex, agent.ReleaseNodeIndex - agent.InNodeIndex + 1); + lockedNodes.AddRange(agent.GivewayNodes.Where(n => !lockedNodes.Any(ln => ln.Id == n.Id))); + } + else + { + nodeIndex = agent.GivewayNodes.FindIndex(n => n.Id == nodeId); + if (nodeIndex != -1) lockedNodes = agent.GivewayNodes.GetRange(nodeIndex, agent.GivewayNodes.Count - nodeIndex); + } + + trafficmap.UpdateLocker(robotId, [.. lockedNodes]); + } + else + { + var nodeIndex = agent.Nodes.FindIndex(n => n.Id == nodeId); + if (nodeIndex != -1) + { + agent.InNodeIndex = nodeIndex; + + if (agent.ReleaseNodeIndex != -1 && agent.InNodeIndex <= agent.ReleaseNodeIndex) + { + var lockedNodes = agent.Nodes.GetRange(agent.InNodeIndex, agent.ReleaseNodeIndex - agent.InNodeIndex + 1); + trafficmap.UpdateLocker(robotId, [.. lockedNodes]); + trafficmap.GivewayResolution.RemoveAll(s => s.RobotReceive == agent.Robot.SerialNumber && s.IsGiveway && !lockedNodes.Any(n => s.Nodes.Any(sn => sn.Id == n.Id))); + } + if (agent.State == TrafficSolutionState.LoopResolve && nodeId == agent.ReleaseNode.Id) + { + var giveWayResolution = trafficmap.GivewayResolution.FirstOrDefault(n => n.RobotGive == robotId); + if (giveWayResolution is not null) + { + trafficmap.AddLocker(giveWayResolution.RobotReceive, [.. giveWayResolution.Nodes]); + giveWayResolution.IsGiveway = true; + } + agent.State = TrafficSolutionState.Waitting; + } + } + } + } + } + return new(true, ""); + } + catch (Exception ex) + { + Logger.Warning($"{robotId} Cập nhật vị trí xảy ra lỗi: {ex.Message}"); + return new(false, $"{robotId} Cập nhật vị trí xảy ra lỗi: {ex.Message}"); + } + finally + { + UpdateSemaphore.Release(); + } + } + else return new(false, $"Không thể xóa cập nhật dữ liệu vào hệ thống traffic do vượt quá thời gian chờ."); + } + + private static (TrafficConflictState state, string robotId, TrafficNodeDto? nodeId) FirstCheckingReleaseNodes(TrafficNodeDto[] releaseNodes, Agent agent, TrafficMap trafficMap) + { + if (releaseNodes.Length != 0) + { + foreach (var releaseNode in releaseNodes) + { + foreach (var locked in trafficMap.Locked) + { + if (locked.Key == agent.Robot.SerialNumber) continue; + if (locked.Value.Any(n => n.Id == releaseNode.Id)) return (TrafficConflictState.Vertex, locked.Key, releaseNode); + //foreach (var lockedNode in locked.Value) + //{ + // var distance = Math.Sqrt(Math.Pow(lockedNode.X - releaseNode.X, 2) + Math.Pow(lockedNode.Y - releaseNode.Y, 2)); + // if(distance > (lockedNode.LockedShapes.Radius + releaseNode.LockedShapes.Radius)) continue; + // if (lockedNode.LockedShapes.Type == TrafficLockedShapeType.Circle) + // { + // if (releaseNode.LockedShapes.Type == TrafficLockedShapeType.Circle) + // { + // if (distance < (lockedNode.LockedShapes.Radius + releaseNode.LockedShapes.Radius)) return (TrafficConflictState.Proximity, locked.Key, releaseNode); + // } + // else if (releaseNode.LockedShapes.Type == TrafficLockedShapeType.Rectangle) + // { + // if(TrafficMath.RectIntersectCircle(lockedNode, lockedNode.LockedShapes.Radius, + // releaseNode.LockedShapes.X1, releaseNode.LockedShapes.Y1, + // releaseNode.LockedShapes.X2, releaseNode.LockedShapes.Y2, + // releaseNode.LockedShapes.X3, releaseNode.LockedShapes.Y3, + // releaseNode.LockedShapes.X4, releaseNode.LockedShapes.Y4)) return (TrafficConflictState.Proximity, locked.Key, releaseNode); + // } + // } + // else if (lockedNode.LockedShapes.Type == TrafficLockedShapeType.Rectangle) + // { + // if (releaseNode.LockedShapes.Type == TrafficLockedShapeType.Circle) + // { + // if(TrafficMath.RectIntersectRect(lockedNode.LockedShapes, releaseNode.LockedShapes)) return (TrafficConflictState.Proximity, locked.Key, releaseNode); + // } + // else if (releaseNode.LockedShapes.Type == TrafficLockedShapeType.Circle) + // { + // if (TrafficMath.RectIntersectCircle(releaseNode, releaseNode.LockedShapes.Radius, + // lockedNode.LockedShapes.X1, lockedNode.LockedShapes.Y1, + // lockedNode.LockedShapes.X2, lockedNode.LockedShapes.Y2, + // lockedNode.LockedShapes.X3, lockedNode.LockedShapes.Y3, + // lockedNode.LockedShapes.X4, lockedNode.LockedShapes.Y4)) return (TrafficConflictState.Proximity, locked.Key, releaseNode); + // } + // } + //} + } + } + } + return (TrafficConflictState.None, string.Empty, null); + } + + private static (bool IsSuccess, string robotId, TrafficNodeDto? nodeId) GivewayCheckingReleaseNodes(TrafficNodeDto[] releaseNodes, string robotId, TrafficMap trafficMap) + { + foreach (var node in releaseNodes) + { + foreach (var locked in trafficMap.Locked) + { + if (locked.Value.Any(n => (n.Id == node.Id)) && locked.Key != robotId) + return (false, locked.Key, node); + } + } + return (true, string.Empty, null); + } + + private void CheckConflict(Agent agent, TrafficMap trafficMap) + { + if (agent.Checking(TrafficAvoidableNodeMax, TrafficCheckingDistanceMax) && !trafficMap.GivewayResolution.Any(s => s.RobotGive == agent.Robot.SerialNumber)) + { + var releaseNodes = agent.GetChekingNodes(TrafficAvoidableNodeMax, TrafficCheckingDistanceMin); + if (releaseNodes.Length > 0) + { + (var state, string conflictRobotId, TrafficNodeDto? conflictNode) = FirstCheckingReleaseNodes(releaseNodes, agent, trafficMap); + if (state == TrafficConflictState.None) + { + + trafficMap.AddLocker(agent.Robot.SerialNumber, [.. releaseNodes]); + agent.ReleaseNode = releaseNodes[^1]; + agent.State = TrafficSolutionState.Complete; + trafficMap.Conflicts.RemoveAll(conflict => conflict.AgentRequest.Robot.SerialNumber == agent.Robot.SerialNumber); + } + else + { + if (conflictNode is null) + { + Logger.Warning($"{agent.Robot.SerialNumber} kiểm tra xung đột có lỗi: xảy ra xung đột nhưng không tìm thấy node xung đột"); + return; + } + + if (trafficMap.Agents.TryGetValue(conflictRobotId, out Agent? conflictAgent) && conflictAgent is not null) + { + trafficMap.Conflicts.RemoveAll(c => c.AgentRequest.Robot.SerialNumber == agent.Robot.SerialNumber); + var conflictState = GetConflictState(agent, conflictAgent, conflictNode); + if (conflictState == TrafficConflictState.Confrontation || state == TrafficConflictState.Proximity) + { + Level1ConflictResolution(agent, conflictAgent, [.. releaseNodes], conflictNode, trafficMap); + } + else + { + + trafficMap.Conflicts.Add(new() + { + AgentConflict = conflictAgent, + AgentRequest = agent, + NodeConflict = conflictNode, + ReleaseNodes = [.. releaseNodes], + State = TrafficConflictState.Vertex, + }); + agent.State = TrafficSolutionState.Waitting; + } + } + else + { + // cần xử lí khi bị xung đột với robot đang không di chuyển + } + } + } + } + } + + private static TrafficConflictState GetConflictState(Agent agent1, Agent agent2, TrafficNodeDto conflictNode) + { + var conflictNodeAgent2Index = agent2.Nodes.IndexOf(conflictNode); + if (conflictNodeAgent2Index != -1) + { + var remainingNodes = agent2.Nodes.GetRange(conflictNodeAgent2Index, agent2.Nodes.Count - conflictNodeAgent2Index); + if (remainingNodes.Any(n => n.Id == agent1.ReleaseNode.Id)) return TrafficConflictState.Confrontation; + return TrafficConflictState.Vertex; + } + + return TrafficConflictState.None; + } + + private TrafficSolutionState Level1ConflictResolution(Agent agent1, Agent agent2, List releaseNodes, TrafficNodeDto conflictNode, TrafficMap trafficMap) + { + try + { + var remainingNodes = agent2.Nodes.GetRange(agent2.InNodeIndex, agent2.Nodes.Count - agent2.InNodeIndex); + var conflictIndex = releaseNodes.IndexOf(conflictNode); + if (conflictIndex != -1) + { + //double distance = 0; + for (int i = conflictIndex - 1; i >= 0; i--) + { + //distance += Math.Sqrt(Math.Pow(releaseNodes[i].X - releaseNodes[i + 1].X, 2) + Math.Pow(releaseNodes[i].Y - releaseNodes[i + 1].Y, 2)); + //if (distance < TrafficLockedCircle) continue; + if (!remainingNodes.Any(n => n.Id == releaseNodes[i].Id)) + { + var givewayNodes = releaseNodes.GetRange(0, releaseNodes.Count - i); + (var IsSuccess, string conflictRobotId, TrafficNodeDto? _) = GivewayCheckingReleaseNodes([.. givewayNodes], agent1.Robot.SerialNumber, trafficMap); + if (IsSuccess) + { + trafficMap.AddLocker(agent1.Robot.SerialNumber, [.. givewayNodes]); + agent1.State = TrafficSolutionState.Complete; + agent1.ReleaseNode = givewayNodes[^1]; + trafficMap.Conflicts.RemoveAll(conflict => conflict.AgentRequest.Robot.SerialNumber == agent1.Robot.SerialNumber); + return TrafficSolutionState.Complete; + } + } + if (!releaseNodes[i].IsAvoidableNode) continue; + + var state = Level2ConflictResolution(agent1, agent2, [.. releaseNodes[i].AvoidablePaths], [.. releaseNodes], [.. releaseNodes.GetRange(0, i + 1)], trafficMap); + if (state != TrafficSolutionState.UnableResolve) return state; + } + if (!trafficMap.GivewayResolution.Any(s => s.RobotReceive == agent1.Robot.SerialNumber)) + { + trafficMap.Conflicts.Add(new() + { + AgentRequest = agent1, + AgentConflict = agent2, + NodeConflict = conflictNode, + ReleaseNodes = releaseNodes, + State = TrafficConflictState.Confrontation, + }); + } + return TrafficSolutionState.UnableResolve; + } + else + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 1 xảy ra lỗi: confilct node không được tìm thấy trong release node {conflictNode.Name}, release list [{string.Join(", ", releaseNodes.Select(n => n.Name))}]"); + } + + return TrafficSolutionState.UnableResolve; + } + catch (Exception ex) + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 1 xảy ra lỗi: {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState Level2ConflictResolution(Agent agent1, Agent agent2, TrafficNodeDto[][] avoidablePaths, TrafficNodeDto[] relaseNodes, TrafficNodeDto[] conflictNodes, TrafficMap trafficMap) + { + try + { + if (agent2.State == TrafficSolutionState.GiveWay) return TrafficSolutionState.Waitting; + List<(Agent conflictAgent, TrafficNodeDto conflictNode)> bufferConflict = []; + var remainingNodeAgent2 = agent2.Nodes.GetRange(agent2.InNodeIndex, agent2.Nodes.Count - agent2.InNodeIndex); + var remainingReleaseNodeAgent2 = agent2.Nodes.GetRange(agent2.InNodeIndex, agent2.ReleaseNodeIndex - agent2.InNodeIndex + 1); + foreach (var avoidablePath in avoidablePaths) + { + if (avoidablePath.Any(avoidableNode => remainingReleaseNodeAgent2.Any(n => n.Id == avoidableNode.Id)) || + remainingNodeAgent2.Any(avoidableNode => avoidableNode.Id == avoidablePath[^1].Id)) continue; + + (var IsSuccess, string conflictRobotId, TrafficNodeDto? conflictNode) = GivewayCheckingReleaseNodes([.. conflictNodes, .. avoidablePath], agent1.Robot.SerialNumber, trafficMap); + if (IsSuccess) + { + TrafficNodeDto[] givewayNodes = [.. conflictNodes, .. avoidablePath.Skip(1)]; + trafficMap.AddLocker(agent1.Robot.SerialNumber, givewayNodes); + var (state, message) = agent1.UpdateGiveWay(givewayNodes, PathPlannger, Map); + Logger.Info($"{agent1.Robot.SerialNumber} tránh Robot {agent2.Robot.SerialNumber} tại Node {givewayNodes[^1].Name} {(state == TrafficSolutionState.Complete || state == TrafficSolutionState.GiveWay ? "thành công" : $"xảy ra lỗi: {message}")}"); + if (state == TrafficSolutionState.GiveWay) + trafficMap.GivewayResolution.Add(new() + { + RobotGive = agent1.Robot.SerialNumber, + RobotReceive = agent2.Robot.SerialNumber, + Nodes = [conflictNodes[^1]] + }); + return state; + } + else + { + if (conflictNode is null) + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 2 có lỗi: xảy ra xung đột nhưng không tìm thấy node xung đột"); + } + else + { + if (trafficMap.Agents.TryGetValue(conflictRobotId, out Agent? conflictAgent) && conflictAgent is not null) + bufferConflict.Add((conflictAgent, conflictNode)); + } + } + } + + foreach (var (conflictAgent, conflictNode) in bufferConflict) + { + var trafficState = Level3ConflictResolution(conflictAgent, agent1.Robot.SerialNumber, conflictNode, relaseNodes, trafficMap, 0); + if (trafficState == TrafficSolutionState.Complete || trafficState == TrafficSolutionState.GiveWay) return trafficState; + } + + //return FindAvoidableResolution(agent1, agent2, trafficMap) ; + return TrafficSolutionState.UnableResolve; + } + catch (Exception ex) + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 2 xảy ra lỗi: {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState Level3ConflictResolution(Agent agent, string giveWayRobotId, TrafficNodeDto giveNode, TrafficNodeDto[] relaseNodes, TrafficMap trafficMap, int counter) + { + try + { + if (agent.State == TrafficSolutionState.GiveWay || agent.State == TrafficSolutionState.LoopResolve) return TrafficSolutionState.Waitting; + List<(Agent conflictAgent, TrafficNodeDto conflictNode)> bufferConflict = []; + + var negativePaths = Map.GetNegativePaths(trafficMap.MapId, new() { Id = giveNode.Id, X = giveNode.X, Y = giveNode.Y, Name = giveNode.Name }, agent.AgentModel.TurningRadius) + .Where(path => path.Length > 0) + .Select(path => path.Select(n => new TrafficNodeDto { Id = n.Id, X = n.X, Y = n.Y, Name = n.Name }).ToArray()) + .ToList(); + + foreach (var negativePath in negativePaths) + { + if (negativePath.Any(negativeNode => relaseNodes.Any(n => n.Id == negativeNode.Id) && negativeNode.Id != giveNode.Id)) continue; + + (var IsSuccess, string conflictRobotId, TrafficNodeDto? conflictNode) = GivewayCheckingReleaseNodes(negativePath, agent.Robot.SerialNumber, trafficMap); + if (IsSuccess) + { + trafficMap.AddLocker(agent.Robot.SerialNumber, negativePath); + var (state, message) = agent.UpdateGiveWay(negativePath, PathPlannger, Map); + Logger.Info($"{agent.Robot.SerialNumber} tránh level3 Robot {giveWayRobotId} tại Node {negativePath[^1].Name} {(state == TrafficSolutionState.Complete || state == TrafficSolutionState.GiveWay ? "thành công" : $"xảy ra lỗi: {message}")}"); + if (state == TrafficSolutionState.GiveWay) + trafficMap.GivewayResolution.Add(new() + { + RobotGive = agent.Robot.SerialNumber, + RobotReceive = giveWayRobotId, + Nodes = [giveNode] + }); + return state; + } + else + { + if (conflictNode is null) + { + Logger.Warning($"{agent.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 3 có lỗi: xảy ra xung đột nhưng không tìm thấy node xung đột"); + } + else + { + if (trafficMap.Agents.TryGetValue(conflictRobotId, out Agent? conflictAgent) && conflictAgent is not null) + bufferConflict.Add((conflictAgent, conflictNode)); + } + } + } + + if (counter < TrafficResolutionRepeat) + { + foreach (var (conflictAgent, conflictNode) in bufferConflict) + { + var trafficState = Level3ConflictResolution(conflictAgent, agent.Robot.SerialNumber, conflictNode, [.. relaseNodes, giveNode], trafficMap, ++counter); + if (trafficState == TrafficSolutionState.Complete || trafficState == TrafficSolutionState.GiveWay) return trafficState; + } + } + return TrafficSolutionState.UnableResolve; + } + catch (Exception ex) + { + Logger.Warning($"{agent.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột bậc 3 xảy ra lỗi: {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState FindAvoidableResolution(Agent agent1, Agent agent2, TrafficMap trafficMap) + { + try + { + if (agent2.State == TrafficSolutionState.GiveWay || agent2.State == TrafficSolutionState.LoopResolve) return TrafficSolutionState.Waitting; + if (agent2.ReleaseNodeIndex == -1 || agent1.ReleaseNodeIndex == -1) return TrafficSolutionState.Waitting; + var conflictNodes = agent2.Nodes.GetRange(agent2.ReleaseNodeIndex, agent2.Nodes.Count - agent2.ReleaseNodeIndex); + List<(Agent conflictAgent, TrafficNodeDto conflictNode)> bufferConflict = []; + for (int i = 1; i < conflictNodes.Count; i++) + { + if (conflictNodes[i].IsAvoidableNode) + { + var remainingNodeAgent2 = agent2.Nodes.GetRange(agent2.ReleaseNodeIndex, agent2.Nodes.Count - agent2.ReleaseNodeIndex); + foreach (var avoidablePath in conflictNodes[i].AvoidablePaths) + { + if (remainingNodeAgent2.Any(n => n.Id == avoidablePath[^1].Id)) continue; + var givewayagent2 = conflictNodes.GetRange(1, i); + var mergeNode = givewayagent2.LastOrDefault(n => agent1.Nodes.Any(a1 => a1.Id == n.Id)); + if (mergeNode is not null) + { + var avoidableIndex = givewayagent2.FindIndex(n => n.Id == mergeNode.Id); + if (avoidableIndex != -1) + { + var giveWayNodes = givewayagent2.GetRange(avoidableIndex, givewayagent2.Count - avoidableIndex - 1); + giveWayNodes.AddRange(avoidablePath); + (var IsSuccess, string conflictRobotId, TrafficNodeDto? conflictNode) = GivewayCheckingReleaseNodes([.. giveWayNodes], agent1.Robot.SerialNumber, trafficMap); + if (IsSuccess) + { + trafficMap.AddLocker(agent1.Robot.SerialNumber, [.. giveWayNodes]); + var (state, message) = agent1.UpdateGiveWay([.. giveWayNodes], PathPlannger, Map); + Logger.Info($"{agent1.Robot.SerialNumber} giải quyết đối đầu với robot {agent2.Robot.SerialNumber} tại node {giveWayNodes[^1].Name} {(state == TrafficSolutionState.Complete || state == TrafficSolutionState.GiveWay ? "thành công" : $"xảy ra lỗi: {message}")}"); + if (state == TrafficSolutionState.GiveWay) + trafficMap.GivewayResolution.Add(new() + { + RobotGive = agent1.Robot.SerialNumber, + RobotReceive = agent2.Robot.SerialNumber, + Nodes = [conflictNodes[i]] + }); + return state; + } + else + { + if (conflictNode is null) + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột đối đầu có lỗi: xảy ra xung đột nhưng không tìm thấy node xung đột"); + } + else + { + if (trafficMap.Agents.TryGetValue(conflictRobotId, out Agent? conflictAgent) && conflictAgent is not null) + bufferConflict.Add((conflictAgent, conflictNode)); + } + } + } + } + } + } + } + foreach (var (conflictAgent, conflictNode) in bufferConflict) + { + var trafficState = Level3ConflictResolution(conflictAgent, agent1.Robot.SerialNumber, conflictNode, [.. conflictNodes], trafficMap, 0); + if (trafficState == TrafficSolutionState.Complete || trafficState == TrafficSolutionState.GiveWay) return trafficState; + } + + return TrafficSolutionState.UnableResolve; + } + catch (Exception ex) + { + Logger.Warning($"{agent1.Robot.SerialNumber} tìm kiếm giải pháp tránh xung đột đối đầu xảy ra lỗi: {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState LoopConflictResolution(TrafficConflict[] conflicts, TrafficMap trafficMap) + { + try + { + for (int i = 0; i < conflicts.Length; i++) + { + var conflictAvoid = conflicts.FirstOrDefault(c => c.AgentConflict.Robot.SerialNumber == conflicts[i].AgentRequest.Robot.SerialNumber); + if (conflictAvoid is null) continue; + var conflictResolutionState = LoopConflictLevel1Resolution(conflicts[i], conflictAvoid, trafficMap); + if (conflictResolutionState == TrafficSolutionState.Complete) return conflictResolutionState; + } + for (int i = 0; i < conflicts.Length; i++) + { + var conflictAvoid = conflicts.FirstOrDefault(c => c.AgentConflict.Robot.SerialNumber == conflicts[i].AgentRequest.Robot.SerialNumber); + if (conflictAvoid is null) continue; + var conflictResolutionState = LoopConflictLevel2Resolution(conflicts[i], conflictAvoid, trafficMap); + if (conflictResolutionState == TrafficSolutionState.Complete) return conflictResolutionState; + } + return TrafficSolutionState.UnableResolve; + } + catch (Exception ex) + { + Logger.Warning($"LoopConflictResolution: Hệ thống xảy ra lỗi - {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState LoopConflictLevel1Resolution(TrafficConflict conflict1, TrafficConflict conflict2, TrafficMap trafficMap) + { + try + { + var agent1ConflictIndex = conflict1.ReleaseNodes.IndexOf(conflict1.NodeConflict); + var agent2ConflictIndex = conflict2.ReleaseNodes.IndexOf(conflict2.NodeConflict); + if (agent1ConflictIndex == -1 || agent2ConflictIndex == -1) return TrafficSolutionState.UnableResolve; + var agent1ConflictNodes = conflict1.ReleaseNodes.GetRange(0, agent1ConflictIndex); + var agent2ConflictNodes = conflict2.ReleaseNodes.GetRange(0, agent2ConflictIndex + 1); + + Logger.Info($"{conflict1.AgentRequest.Robot.SerialNumber} xung đột lặp lại với robot {conflict2.AgentRequest.Robot.SerialNumber} - release 1: [{conflict1.ReleaseNodes[0].Name} => {conflict1.ReleaseNodes[^1].Name} - {conflict1.NodeConflict.Name}] - release 2: [{conflict2.ReleaseNodes[0].Name} => {conflict2.ReleaseNodes[^1].Name} - {conflict2.NodeConflict.Name}]"); + + var avoidableNode = agent1ConflictNodes.FirstOrDefault(n => !agent2ConflictNodes.Any(n2 => n2.Id == n.Id)); + if (avoidableNode is not null) + { + TrafficNodeDto[] releaseNodes = [.. conflict1.ReleaseNodes.GetRange(0, conflict1.ReleaseNodes.IndexOf(avoidableNode) + 1)]; + (var conflictState, string conflictRobotId, TrafficNodeDto? conflictNode) = FirstCheckingReleaseNodes(releaseNodes, conflict1.AgentRequest, trafficMap); + if (conflictState == TrafficConflictState.None) + { + Logger.Info($"{conflict1.AgentRequest.Robot.SerialNumber} giải quyết xung đột lặp lại với robot {conflict2.AgentRequest.Robot.SerialNumber} tại node {avoidableNode.Name} - release {releaseNodes[^1].Name} "); + trafficMap.AddLocker(conflict1.AgentRequest.Robot.SerialNumber, [.. releaseNodes]); + conflict1.AgentRequest.ReleaseNode = releaseNodes[^1]; + conflict1.AgentRequest.State = TrafficSolutionState.LoopResolve; + trafficMap.GivewayResolution.Add(new() + { + RobotGive = conflict1.AgentRequest.Robot.SerialNumber, + RobotReceive = conflict2.AgentRequest.Robot.SerialNumber, + Nodes = [.. agent2ConflictNodes] + }); + return TrafficSolutionState.Complete; + } + } + return TrafficSolutionState.Waitting; + } + catch (Exception ex) + { + Logger.Warning($"LoopConflictLevel1Resolution: Hệ thống xảy ra lỗi - {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private TrafficSolutionState LoopConflictLevel2Resolution(TrafficConflict conflict1, TrafficConflict conflict2, TrafficMap trafficMap) + { + try + { + var agent2ConflictIndex = conflict2.ReleaseNodes.IndexOf(conflict2.NodeConflict); + if (agent2ConflictIndex == -1) return TrafficSolutionState.UnableResolve; + var agent2ConflictNodes = conflict2.ReleaseNodes.GetRange(0, agent2ConflictIndex + 1); + + Logger.Info($"{conflict1.AgentRequest.Robot.SerialNumber} xung đột lặp lại với robot {conflict2.AgentRequest.Robot.SerialNumber} - release 1: [{conflict1.ReleaseNodes[0].Name} => {conflict1.ReleaseNodes[^1].Name} - {conflict1.NodeConflict.Name}] - release 2: [{conflict2.ReleaseNodes[0].Name} => {conflict2.ReleaseNodes[^1].Name} - {conflict2.NodeConflict.Name}]"); + + var avoidableNode = conflict1.ReleaseNodes.FirstOrDefault(n => !agent2ConflictNodes.Any(n2 => n2.Id == n.Id)); + if (avoidableNode is not null) + { + TrafficNodeDto[] releaseNodes = [.. conflict1.ReleaseNodes.GetRange(0, conflict1.ReleaseNodes.IndexOf(avoidableNode) + 1)]; + (var conflictState, string conflictRobotId, TrafficNodeDto? conflictNode) = FirstCheckingReleaseNodes(releaseNodes, conflict1.AgentRequest, trafficMap); + if (conflictState == TrafficConflictState.None) + { + Logger.Info($"{conflict1.AgentRequest.Robot.SerialNumber} giải quyết xung đột lặp lại với robot {conflict2.AgentRequest.Robot.SerialNumber} tại node {avoidableNode.Name} - release {releaseNodes[^1].Name} "); + trafficMap.AddLocker(conflict1.AgentRequest.Robot.SerialNumber, [.. releaseNodes]); + conflict1.AgentRequest.ReleaseNode = releaseNodes[^1]; + conflict1.AgentRequest.State = TrafficSolutionState.LoopResolve; + trafficMap.GivewayResolution.Add(new() + { + RobotGive = conflict1.AgentRequest.Robot.SerialNumber, + RobotReceive = conflict2.AgentRequest.Robot.SerialNumber, + Nodes = [.. agent2ConflictNodes] + }); + return TrafficSolutionState.Complete; + } + else + { + if (trafficMap.Agents.TryGetValue(conflictRobotId, out Agent? conflictAgent) && conflictAgent is not null) + { + var state = Level3ConflictResolution(conflictAgent, conflict1.AgentRequest.Robot.SerialNumber, avoidableNode, [.. releaseNodes, .. agent2ConflictNodes], trafficMap, 0); + if (state == TrafficSolutionState.Complete || state == TrafficSolutionState.GiveWay) + { + trafficMap.Conflicts.RemoveAll(c => c.AgentRequest.Robot.SerialNumber == conflictAgent.Robot.SerialNumber); + return TrafficSolutionState.Complete; + } + return TrafficSolutionState.UnableResolve; + } + } + } + return TrafficSolutionState.Waitting; + } + catch (Exception ex) + { + Logger.Warning($"LoopConflictLevel2Resolution: Hệ thống xảy ra lỗi - {ex.Message}"); + return TrafficSolutionState.UnableResolve; + } + } + + private void ComputeHandler(object? state) + { + try + { + foreach (var trafficmap in TrafficMaps) + { + var agents = trafficmap.Value.Agents.Values.ToList(); + foreach (var agent in agents) + { + CheckConflict(agent, trafficmap.Value); + } + var conflicts = trafficmap.Value.Conflicts.ToList(); + for (int i = 0; i < conflicts.Count; i++) + { + int repeat = 0; + bool isProccessed = false; + string agentConflict = conflicts[i].AgentConflict.Robot.SerialNumber; + List loopConflicts = [conflicts[i]]; + while (repeat++ <= TrafficResolutionRepeat) + { + var loopConflict = conflicts.FirstOrDefault(cl => cl.AgentRequest.Robot.SerialNumber == agentConflict); + if (loopConflict is null) break; + loopConflicts.Add(loopConflict); + if (loopConflict.AgentConflict.Robot.SerialNumber == conflicts[i].AgentRequest.Robot.SerialNumber) + { + if (loopConflicts.Count > 2) + { + var resolutionstate = LoopConflictResolution([.. loopConflicts], trafficmap.Value); + isProccessed = resolutionstate == TrafficSolutionState.Complete; + } + break; + } + else agentConflict = loopConflict.AgentConflict.Robot.SerialNumber; + } + if (isProccessed) break; + + if (conflicts[i].State == TrafficConflictState.Confrontation) + { + if (conflicts.Any(c => c.State == TrafficConflictState.Confrontation && c.AgentRequest.Robot.SerialNumber == conflicts[i].AgentConflict.Robot.SerialNumber)) + { + var resolutionstate = FindAvoidableResolution(conflicts[i].AgentRequest, conflicts[i].AgentConflict, trafficmap.Value); + if (resolutionstate == TrafficSolutionState.Complete || resolutionstate == TrafficSolutionState.GiveWay) + { + trafficmap.Value.Conflicts.Remove(conflicts[i]); + break; + } + } + } + } + } + + computeTimer?.Change(intervalTime, 0); + } + catch (Exception ex) + { + Logger.Warning($"TrafficCompute: Hệ thống xảy ra lỗi - {ex.Message}"); + computeTimer?.Change(intervalTime, 0); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (!Enable) return; + computeTimer = new Timer(ComputeHandler, null, Timeout.Infinite, Timeout.Infinite); + computeTimer.Change(intervalTime, 0); + await Task.Yield(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (computeTimer != null) + { + computeTimer.Change(Timeout.Infinite, Timeout.Infinite); + await computeTimer.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficMap.cs b/RobotNet.RobotManager/Services/Traffic/TrafficMap.cs new file mode 100644 index 0000000..73e4a14 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficMap.cs @@ -0,0 +1,26 @@ +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficMap +{ + public Guid MapId { get; set; } + public Dictionary Agents { get; set; } = []; + public Dictionary> Locked { get; set; } = []; + public List Conflicts { get; set; } = []; + public List GivewayResolution { get; set; } = []; + + public void AddLocker(string robotId, TrafficNodeDto[] releaseNodes) + { + //Console.WriteLine($"{robotId} Add Locker: {string.Join(", ", releaseNodes.Select(n => n.Name))}"); + if (Locked.TryGetValue(robotId, out List? lockedNodes) && lockedNodes is not null) Locked[robotId].AddRange(releaseNodes.Where(n => !lockedNodes.Any(ln => ln.Id == n.Id))); + else Locked.Add(robotId, [.. releaseNodes]); + } + + public void UpdateLocker(string robotId, TrafficNodeDto[] releaseNodes) + { + //Console.WriteLine($"{robotId} Update Locker: {string.Join(", ", releaseNodes.Select(n => n.Name))}"); + if (Locked.TryGetValue(robotId, out _)) Locked[robotId] = [.. releaseNodes]; + else Locked.Add(robotId, [.. releaseNodes]); + } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficMath.cs b/RobotNet.RobotManager/Services/Traffic/TrafficMath.cs new file mode 100644 index 0000000..c86c38f --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficMath.cs @@ -0,0 +1,158 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficMath +{ + public static bool Edge1IntersectEdge(TrafficNodeDto centerCircle, double radius, EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree != MapShares.Enums.TrajectoryDegree.One) return true; + + double dx = edge.X2 - edge.X1; + double dy = edge.Y2 - edge.Y1; + var length = Math.Sqrt(Math.Pow(dx, 2) + Math.Pow(dy, 2)); + if (length == 0) return Math.Sqrt(Math.Pow(edge.X1 - centerCircle.X, 2) + Math.Pow(edge.Y1 - centerCircle.Y, 2)) <= radius; + + double distance = Math.Abs(dy * centerCircle.X - dx * centerCircle.Y + (edge.X2 * edge.Y1 - edge.X1 * edge.Y2)) / length; + if (distance > radius) return false; + + double a = dx * dx + dy * dy; + double b = 2 * (dx * (edge.X1 - centerCircle.X) + dy * (edge.Y1 - centerCircle.Y)); + double c = (edge.X1 - centerCircle.X) * (edge.X1 - centerCircle.X) + (edge.Y1 - centerCircle.Y) * (edge.Y1 - centerCircle.Y) - radius * radius; + + double delta = b * b - 4 * a * c; + if (delta < 0) return false; + + double t1 = (-b + Math.Sqrt(delta)) / (2 * a); + double t2 = (-b - Math.Sqrt(delta)) / (2 * a); + + return (t1 >= 0 && t1 <= 1) || (t2 >= 0 && t2 <= 1); + } + + public static bool Edge2IntersectEdge(TrafficNodeDto centerCircle, double radius, EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree != MapShares.Enums.TrajectoryDegree.Two) return true; + + var length = Math.Sqrt(Math.Pow(edge.X2 - edge.X1, 2) + Math.Pow(edge.Y2 - edge.Y1, 2)); + if (length == 0) return Math.Sqrt(Math.Pow(edge.X1 - centerCircle.X, 2) + Math.Pow(edge.Y1 - centerCircle.Y, 2)) <= radius; + + double step = 0.1 / length; + for (double t = 0; t <= 1.001; t += step) + { + (double x1, double y1) = CurveDegreeTwo(t, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.X2, edge.Y2); + if (Math.Sqrt(Math.Pow(x1 - centerCircle.X, 2) + Math.Pow(y1 - centerCircle.Y, 2)) < radius) return true; + } + return false; + } + + public static bool Edge3IntersectEdge(TrafficNodeDto centerCircle, double radius, EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree != MapShares.Enums.TrajectoryDegree.Three) return true; + + var length = Math.Sqrt(Math.Pow(edge.X2 - edge.X1, 2) + Math.Pow(edge.Y2 - edge.Y1, 2)); + if (length == 0) return Math.Sqrt(Math.Pow(edge.X1 - centerCircle.X, 2) + Math.Pow(edge.Y1 - centerCircle.Y, 2)) <= radius; + + double step = 0.1 / length; + for (double t = 0; t <= 1.001; t += step) + { + (double x1, double y1) = CurveDegreeThree(t, edge.X1, edge.Y1, edge.ControlPoint1X, edge.ControlPoint1Y, edge.ControlPoint2X, edge.ControlPoint2Y, edge.X2, edge.Y2); + if (Math.Sqrt(Math.Pow(x1 - centerCircle.X, 2) + Math.Pow(y1 - centerCircle.Y, 2)) < radius) return true; + } + return false; + } + + public static bool EdgeIntersectCircle(TrafficNodeDto centerCircle, double radius, EdgeCaculatorModel edge) + { + if (edge.TrajectoryDegree == MapShares.Enums.TrajectoryDegree.One) return Edge1IntersectEdge(centerCircle, radius, edge); + else if (edge.TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Two) return Edge2IntersectEdge(centerCircle, radius, edge); + else if (edge.TrajectoryDegree == MapShares.Enums.TrajectoryDegree.Three) return Edge3IntersectEdge(centerCircle, radius, edge); + return true; + } + + public static (double x, double y) CurveDegreeTwo(double t, double x1, double y1, double controlPointX, double controlPointY, double x2, double y2) + { + var x = (1 - t) * (1 - t) * x1 + 2 * t * (1 - t) * controlPointX + t * t * x2; + var y = (1 - t) * (1 - t) * y1 + 2 * t * (1 - t) * controlPointY + t * t * y2; + return (x, y); + } + + public static (double x, double y) CurveDegreeThree(double t, double x1, double y1, double controlPoint1X, double controlPoint1Y, double controlPoint2X, double controlPoint2Y, double x2, double y2) + { + var x = Math.Pow(1 - t, 3) * x1 + 3 * Math.Pow(1 - t, 2) * t * controlPoint1X + 3 * Math.Pow(t, 2) * (1 - t) * controlPoint2X + Math.Pow(t, 3) * x2; ; + var y = Math.Pow(1 - t, 3) * y1 + 3 * Math.Pow(1 - t, 2) * t * controlPoint1Y + 3 * Math.Pow(t, 2) * (1 - t) * controlPoint2Y + Math.Pow(t, 3) * y2; + return (x, y); + } + + public static double CalTurningRadius(AgentModel model, double offsetLength, double offsetWidth) + { + var navigationPointX = offsetLength - model.NavigationPointX; + var navigationPointY = offsetWidth - model.NavigationPointY; + var length = model.Length + 2 * offsetLength; + var width = model.Width + 2 * offsetWidth; + double d1 = Math.Sqrt(navigationPointX * navigationPointX + navigationPointY * navigationPointY); + double d2 = Math.Sqrt((length - navigationPointX) * (length - navigationPointX) + navigationPointY * navigationPointY); + double d3 = Math.Sqrt(navigationPointX * navigationPointX + (width - navigationPointY) * (width - navigationPointY)); + double d4 = Math.Sqrt((length - navigationPointX) * (length - navigationPointX) + (width - navigationPointY) * (width - navigationPointY)); + + return Math.Max(Math.Max(d1, d2), Math.Max(d3, d4)); + } + + public static TrafficLockedShapeDto CalRobotRectShape(AgentModel model, double offsetLength, double offsetWidth, double x, double y, double theta) + { + double x1 = model.NavigationPointX - offsetLength; + double y1 = model.NavigationPointY - offsetWidth; + double x2 = model.Length + model.NavigationPointX + offsetLength; + double y2 = model.NavigationPointY - offsetWidth; + double x3 = model.Length + model.NavigationPointX + offsetLength; + double y3 = model.Width + model.NavigationPointY + offsetWidth; + double x4 = model.NavigationPointX - offsetLength; + double y4 = model.Width + model.NavigationPointY + offsetWidth; + + return new() + { + Type = TrafficLockedShapeType.Rectangle, + X1 = x1 * Math.Cos(theta) - y1 * Math.Sin(theta) + x, + Y1 = x1 * Math.Sin(theta) + y1 * Math.Cos(theta) + y, + X2 = x2 + Math.Cos(theta) - y2 * Math.Sin(theta) + x, + Y2 = x2 * Math.Sin(theta) + y2 * Math.Cos(theta) + y, + X3 = x3 + Math.Cos(theta) - y3 * Math.Sin(theta) + x, + Y3 = x3 * Math.Sin(theta) + y3 * Math.Cos(theta) + y, + X4 = x4 + Math.Cos(theta) - y4 * Math.Sin(theta) + x, + Y4 = x4 * Math.Sin(theta) + y4 * Math.Cos(theta) + y + }; + } + + public static bool RectIntersectCircle(TrafficNodeDto centerCircle, double radius, double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { + double xMin = Math.Min(Math.Min(x1, x2), Math.Min(x3, x4)); + double xMax = Math.Max(Math.Max(x1, x2), Math.Max(x3, x4)); + double yMin = Math.Min(Math.Min(y1, y2), Math.Min(y3, y4)); + double yMax = Math.Max(Math.Max(y1, y2), Math.Max(y3, y4)); + + double xClosest = Math.Max(xMin, Math.Min(centerCircle.X, xMax)); + double yClosest = Math.Max(yMin, Math.Min(centerCircle.Y, yMax)); + + double distance = Math.Sqrt((centerCircle.X - xClosest) * (centerCircle.X - xClosest) + (centerCircle.Y - yClosest) * (centerCircle.Y - yClosest)); + return distance <= radius; + } + + public static bool RectIntersectRect(TrafficLockedShapeDto rect1, TrafficLockedShapeDto rect2) + { + double x1Min = Math.Min(Math.Min(rect1.X1, rect1.X2), Math.Min(rect1.X3, rect1.X4)); + double x1Max = Math.Max(Math.Max(rect1.X1, rect1.X2), Math.Max(rect1.X3, rect1.X4)); + double y1Min = Math.Min(Math.Min(rect1.Y1, rect1.Y2), Math.Min(rect1.Y3, rect1.Y4)); + double y1Max = Math.Max(Math.Max(rect1.Y1, rect1.Y2), Math.Max(rect1.Y3, rect1.Y4)); + + double x2Min = Math.Min(Math.Min(rect2.X1, rect2.X2), Math.Min(rect2.X3, rect2.X4)); + double x2Max = Math.Max(Math.Max(rect2.X1, rect2.X2), Math.Max(rect2.X3, rect2.X4)); + double y2Min = Math.Min(Math.Min(rect2.Y1, rect2.Y2), Math.Min(rect2.Y3, rect2.Y4)); + double y2Max = Math.Max(Math.Max(rect2.Y1, rect2.Y2), Math.Max(rect2.Y3, rect2.Y4)); + + bool xOverlap = x1Min <= x2Max && x2Min <= x1Max; + bool yOverlap = y1Min <= y2Max && y2Min <= y1Max; + + return xOverlap && yOverlap; + } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficPublisher.cs b/RobotNet.RobotManager/Services/Traffic/TrafficPublisher.cs new file mode 100644 index 0000000..7a2dde4 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficPublisher.cs @@ -0,0 +1,84 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.RobotManager.Hubs; +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficPublisher(TrafficManager TrafficManager, IHubContext TrafficHub, LoggerController Logger) : IHostedService +{ + private const int intervalTime = 2000; + private WatchTimer? Timer; + + public readonly Dictionary TrafficMapActive = []; + + public List GetAgents(Guid MapId) + { + if (TrafficManager.TrafficMaps.TryGetValue(MapId, out TrafficMap? trafficMap)) + { + List agents = []; + List lockedNodes = []; + foreach (var agent in trafficMap.Agents) + { + if (trafficMap.Locked.TryGetValue(agent.Key, out List? locked) && locked is not null) lockedNodes = locked; + var trafficConflict = trafficMap.Conflicts.FirstOrDefault(c => c.AgentRequest.Robot.SerialNumber == agent.Key); + agents.Add(new() + { + RobotId = agent.Key, + State = agent.Value.State, + ReleaseNode = agent.Value.ReleaseNode, + Nodes = agent.Value.Nodes, + InNode = agent.Value.Nodes[agent.Value.InNodeIndex], + LockedNodes = lockedNodes, + SubNodes = agent.Value.SubNodes, + GiveWayNodes = agent.Value.GivewayNodes, + ConflictAgentId = trafficConflict is null ? "" : trafficConflict.AgentConflict.Robot.SerialNumber, + ConflictNode = trafficConflict?.NodeConflict, + }); + } + foreach (var agent in trafficMap.Locked) + { + if (agents.Any(a => a.RobotId == agent.Key)) continue; + agents.Add(new() + { + RobotId = agent.Key, + LockedNodes = agent.Value, + }); + } + return agents; + } + return []; + } + + private void TimerHandler() + { + try + { + foreach (var mapActive in TrafficMapActive) + { + var agents = GetAgents(mapActive.Key); + if (agents.Count > 0) + { + var pub = Task.Run(async () => await TrafficHub.Clients.Client(mapActive.Value).SendAsync("TrafficAgentUpdated", agents)); + pub.Wait(); + } + } + } + catch (Exception ex) + { + Logger.Error($"Robot Publisher Error: {ex}"); + } + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + Timer = new(intervalTime, TimerHandler, Logger); + Timer.Start(); + await Task.Yield(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Timer?.Dispose(); + return Task.CompletedTask; + } +} diff --git a/RobotNet.RobotManager/Services/Traffic/TrafficSolution.cs b/RobotNet.RobotManager/Services/Traffic/TrafficSolution.cs new file mode 100644 index 0000000..3cd79a3 --- /dev/null +++ b/RobotNet.RobotManager/Services/Traffic/TrafficSolution.cs @@ -0,0 +1,14 @@ +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotManager.Services.Traffic; + +public class TrafficSolution +{ + public TrafficSolutionState State { get; set; } + public TrafficNodeDto[] Nodes { get; set; } = []; + public TrafficEdgeDto[] Edges { get; set; } = []; + public TrafficNodeDto ReleaseNode { get; set; } = new(); + public List GivewayNodes { get; set; } = []; + public List GivewayEdges { get; set; } = []; +} diff --git a/RobotNet.RobotManager/Services/WatchTimer.cs b/RobotNet.RobotManager/Services/WatchTimer.cs new file mode 100644 index 0000000..fde47d0 --- /dev/null +++ b/RobotNet.RobotManager/Services/WatchTimer.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; + +namespace RobotNet.RobotManager.Services; + +public class WatchTimer(int Interval, Action Callback, LoggerController? Logger) : IDisposable where T : class +{ + private System.Threading.Timer? Timer; + private readonly Stopwatch Watch = new(); + public bool Disposed; + + private void Handler(object? state) + { + try + { + Watch.Restart(); + + Callback.Invoke(); + + Watch.Stop(); + if (Watch.ElapsedMilliseconds >= Interval || Interval - Watch.ElapsedMilliseconds <= 50) + { + Timer?.Change(Interval, Timeout.Infinite); + } + else + { + Timer?.Change(Interval - Watch.ElapsedMilliseconds, Timeout.Infinite); + } + } + catch (Exception ex) + { + Logger?.Error($"WatchTimer Error: {ex}"); + Timer?.Change(Interval, Timeout.Infinite); + } + } + + public void Start() + { + if (!Disposed) + { + Timer = new Timer(Handler, null, Timeout.Infinite, Timeout.Infinite); + Timer.Change(Interval, 0); + } + else throw new ObjectDisposedException(nameof(WatchTimer)); + } + + public void Stop() + { + if (Disposed) return; + if (Timer != null) + { + Timer.Change(Timeout.Infinite, Timeout.Infinite); + Timer.Dispose(); + Timer = null; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) Stop(); + + Disposed = true; + } + + ~WatchTimer() + { + Dispose(false); + } +} diff --git a/RobotNet.RobotManager/Services/WatchTimerAsync.cs b/RobotNet.RobotManager/Services/WatchTimerAsync.cs new file mode 100644 index 0000000..2306e60 --- /dev/null +++ b/RobotNet.RobotManager/Services/WatchTimerAsync.cs @@ -0,0 +1,76 @@ +using System.Diagnostics; + +namespace RobotNet.RobotManager.Services; + +public class WatchTimerAsync(int Interval, Func Callback, LoggerController? Logger) : IDisposable where T : class +{ + private System.Threading.Timer? Timer; + private readonly Stopwatch Watch = new(); + public bool Disposed; + + private async void Handler(object? state) + { + try + { + Watch.Restart(); + + await Callback.Invoke(); + + Watch.Stop(); + if (Watch.ElapsedMilliseconds >= Interval || Interval - Watch.ElapsedMilliseconds <= 50) + { + Timer?.Change(Interval, Timeout.Infinite); + } + else + { + Timer?.Change(Interval - Watch.ElapsedMilliseconds, Timeout.Infinite); + } + } + catch (Exception ex) + { + Logger?.Error($"WatchTimer Error: {ex}"); + Timer?.Change(Interval, Timeout.Infinite); + } + } + + public void Start() + { + if (!Disposed) + { + Timer = new Timer(Handler, null, Timeout.Infinite, Timeout.Infinite); + Timer.Change(Interval, 0); + } + else throw new ObjectDisposedException(nameof(WatchTimerAsync)); + } + + public void Stop() + { + if (Disposed) return; + if (Timer != null) + { + Timer.Change(Timeout.Infinite, Timeout.Infinite); + Timer.Dispose(); + Timer = null; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) return; + + if (disposing) Stop(); + + Disposed = true; + } + + ~WatchTimerAsync() + { + Dispose(false); + } +} diff --git a/RobotNet.RobotManager/appsettings.json b/RobotNet.RobotManager/appsettings.json new file mode 100644 index 0000000..84fb4c7 --- /dev/null +++ b/RobotNet.RobotManager/appsettings.json @@ -0,0 +1,78 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=172.20.235.170;Database=RobotNet.RobotEditor;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "System.Net.Http.HttpClient": "Warning", + "OpenIddict.Validation.OpenIddictValidationDispatcher": "Warning", + "OpenIddict.Client.OpenIddictClientDispatcher": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Warning", + "Polly": "Warning" + } + }, + "AllowedHosts": "*", + "MinIO": { + "UsingLocal": false, + "Endpoint": "172.20.235.170:9000", + "Bucket": "mapeditor", + "User": "minio", + "Password": "robotics" + }, + "PathPlanning": { + "ResolutionSplit": 0.1, + "Type": "Angle", // Angle, StartDirection, EndDirection with Diffirental and Omidrive Model + "StartDirection": "None", // None, FORWARD, BACKWARD, NON Ewith Diffirental and Omidrive Model + "EndDirection": "None" // None, FORWARD, BACKWARD, NON Ewith Diffirental and Omidrive Model + }, + "VDA5050Setting": { + "ServerEnable": true, + "HostServer": "127.0.0.1", + "Port": "1883", + "UserName": "robotics", + "PassWord": "robotics", + "Manufacturer": "phenikaaX", + "Version": "0.0.1", + "Repeat": 2, + "ConnectionTimeoutSeconds": 30, + "ConnectionBacklog": 10, + "KeepAliveInterval": 60, + "CheckingRobotMsgTimout": 60 + }, + "OpenIddictClientProviderOptions": { + "Issuer": "https://localhost:7061/", + "Audiences": [ + "robotnet-robot-manager" + ], + "ClientId": "robotnet-robot-manager", + "ClientSecret": "469B2DEB-660E-4C91-97C7-D69550D9969D", + "Scopes": [ + "robotnet-map-api" + ] + }, + "MapManager": { + "Url": "https://localhost:7177", + "Scopes": [ + "robotnet-map-api" + ] + }, + "ACSStatusConfig": { + "SiteCode": "VN03", + "AreaCode": "DA3_FL1", + "AreaArea": "DA3_WM", + "BatteryId": "bidpnkx0001" + }, + "TrafficConfig": { + "Enable": false, + "IntervalTime": 1000, + "CheckingDistanceMax": 10, + "CheckingDistanceMin": 5, + "OffsetLength": 0.2, + "OffsetWidth": 0.2, + "ResolutionRepeat": 20, + "AvoidableNodeMax": 3, + "DeviationDistance": 0.5 + } +} diff --git a/RobotNet.RobotManager/nlog.config b/RobotNet.RobotManager/nlog.config new file mode 100644 index 0000000..95e19a8 --- /dev/null +++ b/RobotNet.RobotManager/nlog.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.RobotShares/Dtos/RobotActionDto.cs b/RobotNet.RobotShares/Dtos/RobotActionDto.cs new file mode 100644 index 0000000..54717bd --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotActionDto.cs @@ -0,0 +1,13 @@ +using RobotNet.RobotShares.VDA5050.State; + +namespace RobotNet.RobotShares.Dtos; + +public class RobotActionDto +{ + public string ActionId { get; set; } = ""; + public ActionState? Action { get; set; } + public bool IsError { get; set; } + public bool IsCompleted { get; set; } + public bool IsProcessing { get; set; } + public string[] Errors { get; set; } = []; +} diff --git a/RobotNet.RobotShares/Dtos/RobotDto.cs b/RobotNet.RobotShares/Dtos/RobotDto.cs new file mode 100644 index 0000000..1492b37 --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotDto.cs @@ -0,0 +1,37 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotShares.Dtos; + + +#nullable disable +public class RobotDto +{ + public Guid Id { get; set; } + public string RobotId { get; set; } + public string Name { get; set; } + public Guid ModelId { get; set; } + public string ModelName { get; set; } + public Guid MapId { get; set; } + public string MapName { get; set; } + public NavigationType NavigationType { get; set; } + public bool Online { get; set; } + public string State { get; set; } + public double Battery { get; set; } +} + + +public class RobotCreateModel +{ + public string RobotId { get; set; } + public string Name { get; set; } + public Guid ModelId { get; set; } +} + +public class RobotUpdateModel +{ + public Guid Id { get; set; } + public string RobotId { get; set; } + public string Name { get; set; } + public Guid ModelId { get; set; } + public Guid MapId { get; set; } +} diff --git a/RobotNet.RobotShares/Dtos/RobotInfomationDto.cs b/RobotNet.RobotShares/Dtos/RobotInfomationDto.cs new file mode 100644 index 0000000..50b202c --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotInfomationDto.cs @@ -0,0 +1,44 @@ +using RobotNet.RobotShares.VDA5050.Order; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; + +namespace RobotNet.RobotShares.Dtos; + +#nullable disable + +public class RobotInfomationDto +{ + public string RobotId { get; set; } + public string Name { get; set; } + public Guid MapId { get; set; } + public Load[] Loads { get; set; } = []; + public BatteryState Battery { get; set; } = new(); + public Error[] Errors { get; set; } = []; + public Information[] Infomations { get; set; } = []; + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity AgvVelocity { get; set; } = new(); + public Navigation Navigation { get; set; } = new(); +} + +public class Navigation +{ + public string NavigationState { get; set; } = string.Empty; + public NavigationPathEdge[] RobotPath { get; set; } = []; + public NavigationPathEdge[] RobotBasePath { get; set; } = []; + public List LaserScaner { get; set; } = []; +} + +public record NavigationPathNode(double X, double Y); + +public class NavigationPathEdge() +{ + public double StartX { get; set; } + public double StartY { get; set; } + public double EndX { get; set; } + public double EndY { get; set; } + public double ControlPoint1X { get; set; } + public double ControlPoint1Y { get; set; } + public double ControlPoint2X { get; set; } + public double ControlPoint2Y { get; set; } + public int Degree { get; set; } +} \ No newline at end of file diff --git a/RobotNet.RobotShares/Dtos/RobotModelDto.cs b/RobotNet.RobotShares/Dtos/RobotModelDto.cs new file mode 100644 index 0000000..4e4da48 --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotModelDto.cs @@ -0,0 +1,37 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotShares.Dtos; + +#nullable disable +public class RobotModelDto +{ + public Guid Id { get; set; } + public string ModelName { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double ImageWidth { get; set; } + public double ImageHeight { get; set; } + public double Width { get; set; } + public double Length { get; set; } + public NavigationType NavigationType { get; set; } +} + +public class RobotModelCreateModel +{ + public string ModelName { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Width { get; set; } + public double Length { get; set; } + public NavigationType NavigationType { get; set; } +} + +public class RobotModelUpdateModel +{ + public Guid Id { get; set; } + public string ModelName { get; set; } + public double OriginX { get; set; } + public double OriginY { get; set; } + public double Width { get; set; } + public double Length { get; set; } +} diff --git a/RobotNet.RobotShares/Dtos/RobotOnlineStateDto.cs b/RobotNet.RobotShares/Dtos/RobotOnlineStateDto.cs new file mode 100644 index 0000000..a8ce933 --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotOnlineStateDto.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.Dtos; + +#nullable disable + +public class RobotOnlineStateDto +{ + public string RobotId { get; set; } + public string State { get; set; } + public bool IsOnline { get; set; } + public double Battery { get; set; } +} diff --git a/RobotNet.RobotShares/Dtos/RobotOrderDto.cs b/RobotNet.RobotShares/Dtos/RobotOrderDto.cs new file mode 100644 index 0000000..5cf97ab --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotOrderDto.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.Dtos; + +public class RobotOrderDto +{ + public string OrderId { get; set; } = ""; + public bool IsError { get; set; } + public bool IsCompleted { get; set; } + public bool IsProcessing { get; set; } + public bool IsCanceled { get; set; } + public string[] Errors { get; set; } = []; +} diff --git a/RobotNet.RobotShares/Dtos/RobotVDA5050StateDto.cs b/RobotNet.RobotShares/Dtos/RobotVDA5050StateDto.cs new file mode 100644 index 0000000..1b1d48d --- /dev/null +++ b/RobotNet.RobotShares/Dtos/RobotVDA5050StateDto.cs @@ -0,0 +1,18 @@ +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; + +namespace RobotNet.RobotShares.Dtos; + +#nullable disable + +public class RobotVDA5050StateDto +{ + public string RobotId { get; set; } + public string Name { get; set; } + public Guid MapId { get; set; } + public bool Online { get; set; } + public bool IsWorking { get; set; } + public StateMsg State { get; set; } = new(); + public RobotOrderDto OrderState { get; set; } = new(); + public VisualizationMsg Visualization { get; set; } = new(); +} diff --git a/RobotNet.RobotShares/Dtos/TrafficAgentDto.cs b/RobotNet.RobotShares/Dtos/TrafficAgentDto.cs new file mode 100644 index 0000000..77b1dbe --- /dev/null +++ b/RobotNet.RobotShares/Dtos/TrafficAgentDto.cs @@ -0,0 +1,17 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotShares.Dtos; + +public class TrafficAgentDto +{ + public string RobotId { get; set; } = ""; + public TrafficSolutionState State { get; set; } + public TrafficNodeDto? InNode { get; set; } + public List Nodes { get; set; } = []; + public TrafficNodeDto? ReleaseNode { get; set; } + public List LockedNodes { get; set; } = []; + public List SubNodes { get; set; } = []; + public List GiveWayNodes { get; set; } = []; + public TrafficNodeDto? ConflictNode { get; set; } + public string ConflictAgentId { get; set; } = ""; +} diff --git a/RobotNet.RobotShares/Dtos/TrafficLockedShapeDto.cs b/RobotNet.RobotShares/Dtos/TrafficLockedShapeDto.cs new file mode 100644 index 0000000..f96b5f0 --- /dev/null +++ b/RobotNet.RobotShares/Dtos/TrafficLockedShapeDto.cs @@ -0,0 +1,22 @@ +namespace RobotNet.RobotShares.Dtos; + +public enum TrafficLockedShapeType +{ + None, + Circle, + Rectangle, + Polygon +} +public class TrafficLockedShapeDto +{ + public TrafficLockedShapeType Type { get; set; } + public double Radius { get; set; } + public double X1 { get; set; } + public double Y1 { get; set; } + public double X2 { get; set; } + public double Y2 { get; set; } + public double X3 { get; set; } + public double Y3 { get; set; } + public double X4 { get; set; } + public double Y4 { get; set; } +} diff --git a/RobotNet.RobotShares/Dtos/TrafficMapDto.cs b/RobotNet.RobotShares/Dtos/TrafficMapDto.cs new file mode 100644 index 0000000..a4481f9 --- /dev/null +++ b/RobotNet.RobotShares/Dtos/TrafficMapDto.cs @@ -0,0 +1,13 @@ +namespace RobotNet.RobotShares.Dtos; + +public class TrafficMapDto +{ + public Guid MapId { get; set; } + public string MapName { get; set; } = ""; + public List Agents { get; set; } = []; + + public override string ToString() + { + return MapName; + } +} diff --git a/RobotNet.RobotShares/Dtos/TrafficNodeDto.cs b/RobotNet.RobotShares/Dtos/TrafficNodeDto.cs new file mode 100644 index 0000000..4d4453a --- /dev/null +++ b/RobotNet.RobotShares/Dtos/TrafficNodeDto.cs @@ -0,0 +1,27 @@ +using RobotNet.RobotShares.Enums; + +namespace RobotNet.RobotShares.Dtos; + +public class TrafficNodeDto +{ + public Guid Id { get; set; } + public string Name { get; set; } = ""; + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public bool IsAvoidableNode { get; set; } + public RobotDirection Direction { get; set; } + public TrafficNodeDto[][] AvoidablePaths { get; set; } = []; + public TrafficLockedShapeDto LockedShapes { get; set; } = new(); + public override bool Equals(object? obj) + { + if (obj is TrafficNodeDto other) + return Id == other.Id; + return false; + } + + public override int GetHashCode() + { + return HashCode.Combine(Id); + } +} diff --git a/RobotNet.RobotShares/Enums/MonitorToolbarButtonType.cs b/RobotNet.RobotShares/Enums/MonitorToolbarButtonType.cs new file mode 100644 index 0000000..6afea9d --- /dev/null +++ b/RobotNet.RobotShares/Enums/MonitorToolbarButtonType.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.Enums; + +public enum MonitorToolbarButtonType +{ + Fit, + Focus, + ZoomIn, + ZoomOut +} \ No newline at end of file diff --git a/RobotNet.RobotShares/Enums/MonitorToolbarCheckedType.cs b/RobotNet.RobotShares/Enums/MonitorToolbarCheckedType.cs new file mode 100644 index 0000000..02a4bbd --- /dev/null +++ b/RobotNet.RobotShares/Enums/MonitorToolbarCheckedType.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.Enums; + +public enum MonitorToolbarCheckedType +{ + FocusRobot, + ShowPath, + ShowName, + ShowGrid, + ShowLaser, + ShowElement, +} \ No newline at end of file diff --git a/RobotNet.RobotShares/Enums/NavigationType.cs b/RobotNet.RobotShares/Enums/NavigationType.cs new file mode 100644 index 0000000..f5f4634 --- /dev/null +++ b/RobotNet.RobotShares/Enums/NavigationType.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.Enums; + +public enum NavigationType +{ + Differential, + Forklift, + OmniDrive, + GridDifferential +} diff --git a/RobotNet.RobotShares/Enums/RefreshPathState.cs b/RobotNet.RobotShares/Enums/RefreshPathState.cs new file mode 100644 index 0000000..18962b9 --- /dev/null +++ b/RobotNet.RobotShares/Enums/RefreshPathState.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.Enums; + +public enum RefreshPathState +{ + Created, + Refreshing, + Compeleted, + Error, +} diff --git a/RobotNet.RobotShares/Enums/RobotDirection.cs b/RobotNet.RobotShares/Enums/RobotDirection.cs new file mode 100644 index 0000000..ef6bc4c --- /dev/null +++ b/RobotNet.RobotShares/Enums/RobotDirection.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.Enums; + +public enum RobotDirection +{ + FORWARD, + BACKWARD, + NONE +} + diff --git a/RobotNet.RobotShares/Enums/TrafficConflictState.cs b/RobotNet.RobotShares/Enums/TrafficConflictState.cs new file mode 100644 index 0000000..5c966e9 --- /dev/null +++ b/RobotNet.RobotShares/Enums/TrafficConflictState.cs @@ -0,0 +1,49 @@ +namespace RobotNet.RobotShares.Enums; + +public enum TrafficConflictState +{ + /// + /// Xảy ra khi hai robot sử dụng cùng một cạnh (edge) trong biểu đồ đường đi (graph) trong khoảng thời gian trùng lặp, đồng thời lộ trình tiếp theo của robot chồng lên nhau. + /// + Confrontation, + + /// + /// Xảy ra khi hai robot sử dụng cùng một cạnh (edge) trong biểu đồ đường đi (graph) trong khoảng thời gian trùng lặp nhưng lộ trình tiếp theo của 2 robot không chồng lấn lên nhau. + /// + Edge, + + /// + /// Xảy ra khi hai robot chiếm cùng một nút (vertex/node) trong biểu đồ đường đi tại cùng một thời điểm hoặc trong khoảng thời gian trùng lặp. + /// + Vertex, + + /// + /// Xảy ra khi hai robot ở quá gần nhau (dựa trên khoảng cách Euclidean) trong không gian liên tục, vi phạm khoảng cách an toàn (minDistance). + /// + Proximity, + + /// + /// Xảy ra khi hai robot di chuyển qua một hành lang hẹp (thường được biểu diễn bằng một chuỗi cạnh hoặc node) theo hướng ngược nhau, dẫn đến tình trạng không thể vượt qua nhau. + /// + Corridor, + + /// + /// Xảy ra khi hai robot có lộ trình giao nhau về mặt thời gian, nhưng không nhất thiết ở cùng một cạnh hoặc nút, mà ở các vị trí khiến chúng không thể di chuyển tiếp mà không va chạm. + /// + Temporal, + + /// + /// Xảy ra khi hai robot cần xoay tại một điểm (thường là node) và không gian xoay bị chồng lấn, dẫn đến va chạm hoặc cản trở. + /// + Rotation, + + /// + /// Xảy ra khi các robot cạnh tranh cho một tài nguyên chung (ví dụ: một khu vực làm việc, điểm sạc, hoặc thiết bị nâng) + /// + Resource, + + /// + /// Xảy ra lỗi khi kiểm tra xung đột + /// + None +} diff --git a/RobotNet.RobotShares/Enums/TrafficSolutionState.cs b/RobotNet.RobotShares/Enums/TrafficSolutionState.cs new file mode 100644 index 0000000..ba12a8e --- /dev/null +++ b/RobotNet.RobotShares/Enums/TrafficSolutionState.cs @@ -0,0 +1,12 @@ +namespace RobotNet.RobotShares.Enums; + +public enum TrafficSolutionState +{ + None, + Complete, + Waitting, + GiveWay, + RefreshPath, + LoopResolve, + UnableResolve +} diff --git a/RobotNet.RobotShares/Models/RobotInstantActionModel.cs b/RobotNet.RobotShares/Models/RobotInstantActionModel.cs new file mode 100644 index 0000000..ace7755 --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotInstantActionModel.cs @@ -0,0 +1,7 @@ +namespace RobotNet.RobotShares.Models; + +public class RobotInstantActionModel +{ + public string RobotId { get; set; } = string.Empty; + public RobotShares.VDA5050.InstantAction.Action Action { get; set; } = new(); +} \ No newline at end of file diff --git a/RobotNet.RobotShares/Models/RobotMoveStraightModel.cs b/RobotNet.RobotShares/Models/RobotMoveStraightModel.cs new file mode 100644 index 0000000..5416b91 --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotMoveStraightModel.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotShares.Models; + +public class RobotMoveStraightModel +{ + public string RobotId { get; set; } = string.Empty; + public double X { get; set; } + public double Y { get; set; } +} diff --git a/RobotNet.RobotShares/Models/RobotMoveToNodeModel.cs b/RobotNet.RobotShares/Models/RobotMoveToNodeModel.cs new file mode 100644 index 0000000..df62b7e --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotMoveToNodeModel.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.Models; + +public class RobotMoveToNodeModel +{ + public string RobotId { get; set; } = string.Empty; + public string NodeName { get; set; } = string.Empty; + public double LastAngle { get; set; } + public bool OverrideLastAngle { get; set; } + public IDictionary>? Actions { get; set; } +} + diff --git a/RobotNet.RobotShares/Models/RobotRotateModel.cs b/RobotNet.RobotShares/Models/RobotRotateModel.cs new file mode 100644 index 0000000..2a31417 --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotRotateModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.Models; + +public class RobotRotateModel +{ + public string RobotId { get; set; } = string.Empty; + + [Range(-180, 180)] + public double Angle { get; set; } +} diff --git a/RobotNet.RobotShares/Models/RobotSearchExpressionModel.cs b/RobotNet.RobotShares/Models/RobotSearchExpressionModel.cs new file mode 100644 index 0000000..cf80121 --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotSearchExpressionModel.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotShares.Models; + +public class RobotSearchExpressionModel +{ + public string MapName { get; set; } = string.Empty; + public string ModelName { get; set; } = string.Empty; + public string Expression { get; set; } = string.Empty; +} diff --git a/RobotNet.RobotShares/Models/RobotStateModel.cs b/RobotNet.RobotShares/Models/RobotStateModel.cs new file mode 100644 index 0000000..628601d --- /dev/null +++ b/RobotNet.RobotShares/Models/RobotStateModel.cs @@ -0,0 +1,25 @@ +using RobotNet.RobotShares.Dtos; +using RobotNet.RobotShares.VDA5050.State; +using RobotNet.RobotShares.VDA5050.Visualization; + +namespace RobotNet.RobotShares.Models; + +public class RobotStateModel +{ + public string RobotId { get; set; } = string.Empty; + public Guid MapId { get; set; } + public bool IsOnline { get; set; } + public string State { get; set; } = string.Empty; + public bool NewBaseRequest { get; set; } + public RobotOrderDto OrderState { get; set; } = new(); + public RobotActionDto[] ActionStates { get; set; } = []; + public NodeState[] NodeStates { get; set; } = []; + public EdgeState[] EdgeStates { get; set; } = []; + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity Velocity { get; set; } = new(); + public Load[] Loads { get; set; } = []; + public BatteryState BatteryState { get; set; } = new(); + public Error[] Errors { get; set; } = []; + public Information[] Information { get; set; } = []; + public SafetyState SafetyState { get; set; } = new(); +} \ No newline at end of file diff --git a/RobotNet.RobotShares/Models/UpdateAgentLockerModel.cs b/RobotNet.RobotShares/Models/UpdateAgentLockerModel.cs new file mode 100644 index 0000000..5986401 --- /dev/null +++ b/RobotNet.RobotShares/Models/UpdateAgentLockerModel.cs @@ -0,0 +1,10 @@ +using RobotNet.RobotShares.Dtos; + +namespace RobotNet.RobotShares.Models; + +public class UpdateAgentLockerModel +{ + public Guid MapId { get; set; } + public string AgentId { get; set; } = ""; + public List LockedNodes { get; set; } = []; +} diff --git a/RobotNet.RobotShares/OpenACS/OpenACSPublishSettingDto.cs b/RobotNet.RobotShares/OpenACS/OpenACSPublishSettingDto.cs new file mode 100644 index 0000000..323c834 --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/OpenACSPublishSettingDto.cs @@ -0,0 +1,19 @@ +namespace RobotNet.RobotShares.OpenACS; + +public class OpenACSPublishSettingDto +{ + public string PublishUrl { get; set; } = ""; + public string[] PublishUrlsUsed { get; set; } = []; + public bool IsPublishEnabled { get; set; } + public int PublishInterval { get; set; } + public OpenACSPublishSettingDto() { } + public OpenACSPublishSettingDto(bool enable, string url, string[] urlsUsed, int interval) + { + IsPublishEnabled = enable; + PublishUrl = url; + PublishUrlsUsed = urlsUsed; + PublishInterval = interval; + } +} + +public record class OpenACSPublishSettingModel(string Url, int Interval); \ No newline at end of file diff --git a/RobotNet.RobotShares/OpenACS/OpenACSSettingsDto.cs b/RobotNet.RobotShares/OpenACS/OpenACSSettingsDto.cs new file mode 100644 index 0000000..629169a --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/OpenACSSettingsDto.cs @@ -0,0 +1,7 @@ +namespace RobotNet.RobotShares.OpenACS; + +public class OpenACSSettingsDto +{ + public OpenACSTrafficSettingDto TrafficSetting { get; set; } = new(false, "", []); + public OpenACSPublishSettingDto PublishSetting { get; set; } = new(false, "", [], 2000); +} diff --git a/RobotNet.RobotShares/OpenACS/OpenACSTrafficSettingDto.cs b/RobotNet.RobotShares/OpenACS/OpenACSTrafficSettingDto.cs new file mode 100644 index 0000000..f7d5849 --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/OpenACSTrafficSettingDto.cs @@ -0,0 +1,17 @@ +namespace RobotNet.RobotShares.OpenACS; + +public class OpenACSTrafficSettingDto +{ + public string TrafficUrl { get; set; } = ""; + public string[] PublishUrlsUsed { get; set; } = []; + public bool IsTrafficEnabled { get; set; } + public OpenACSTrafficSettingDto() { } + public OpenACSTrafficSettingDto(bool enable, string url, string[] urlsUsed) + { + IsTrafficEnabled = enable; + TrafficUrl = url; + PublishUrlsUsed = urlsUsed; + } +} + +public record class OpenACSTrafficSettingModel(string Url); \ No newline at end of file diff --git a/RobotNet.RobotShares/OpenACS/RobotACSLockedDto.cs b/RobotNet.RobotShares/OpenACS/RobotACSLockedDto.cs new file mode 100644 index 0000000..ef5f9dd --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/RobotACSLockedDto.cs @@ -0,0 +1,7 @@ +namespace RobotNet.RobotShares.OpenACS; + +public class RobotACSLockedDto +{ + public string RobotId { get; set; } = ""; + public string[] ZoneIds { get; set; } = []; +} diff --git a/RobotNet.RobotShares/OpenACS/TrafficACSRequestModel.cs b/RobotNet.RobotShares/OpenACS/TrafficACSRequestModel.cs new file mode 100644 index 0000000..274197e --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/TrafficACSRequestModel.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotShares.OpenACS; + +public class TrafficACSRequestModel +{ + public string RobotId { get; set; } = ""; + public string ZoneId { get; set; } = ""; + public TrafficRequestType Type { get; set; } = TrafficRequestType.OUT; +} diff --git a/RobotNet.RobotShares/OpenACS/TrafficRequestType.cs b/RobotNet.RobotShares/OpenACS/TrafficRequestType.cs new file mode 100644 index 0000000..0233a50 --- /dev/null +++ b/RobotNet.RobotShares/OpenACS/TrafficRequestType.cs @@ -0,0 +1,20 @@ +namespace RobotNet.RobotShares.OpenACS; + +public enum TrafficRequestType +{ + IN, + OUT, +} + +public static class EnumExtensions +{ + public static string ToInOutString(this TrafficRequestType type) + { + return type switch + { + TrafficRequestType.IN => "in", + TrafficRequestType.OUT => "out", + _ => type.ToString() + }; + } +} \ No newline at end of file diff --git a/RobotNet.RobotShares/RobotNet.RobotShares.csproj b/RobotNet.RobotShares/RobotNet.RobotShares.csproj new file mode 100644 index 0000000..c69b326 --- /dev/null +++ b/RobotNet.RobotShares/RobotNet.RobotShares.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/RobotNet.RobotShares/VDA5050/Connection/ConnectionMsg.cs b/RobotNet.RobotShares/VDA5050/Connection/ConnectionMsg.cs new file mode 100644 index 0000000..db0f412 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Connection/ConnectionMsg.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Connection; + +#nullable disable + +public enum ConnectionState +{ + ONLINE, + OFFLINE, + CONNECTIONBROKEN +} + +public class ConnectionMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string Version { get; set; } = ""; + [Required] + public string Manufacturer { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string ConnectionState { get; set; } = ""; +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/ActionParameters.cs b/RobotNet.RobotShares/VDA5050/Factsheet/ActionParameters.cs new file mode 100644 index 0000000..2282c7b --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/ActionParameters.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public enum ValueDataType +{ + BOOL, + NUMBER, + INTEGER, + FLOAT, + STRING, + OBJECT, + ARRAY, +} +public class ActionParameters +{ + [Required] + public string Key { get; set; } + [Required] + public string ValueDataType { get; set; } + public string Description { get; set; } + public bool IsOptional { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/AgvActions.cs b/RobotNet.RobotShares/VDA5050/Factsheet/AgvActions.cs new file mode 100644 index 0000000..a6eff7f --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/AgvActions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public enum ActionScopes +{ + INSTANT, + NODE, + EDGE, +} +public class AgvActions +{ + [Required] + public string ActionType { get; set; } + public string ActionDescription { get; set; } + [Required] + public string[] ActionScopes { get; set; } + public ActionParameters[] ActionParameters { get; set; } + public string ResultDescription { get; set; } + public string[] BlockingTypes { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/AgvGeometry.cs b/RobotNet.RobotShares/VDA5050/Factsheet/AgvGeometry.cs new file mode 100644 index 0000000..856992f --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/AgvGeometry.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class AgvGeometry +{ + public WheelDefinitions[] WheelDefinitions { get; set; } + public Envelopes2d[] Envelopes2d { get; set; } + public Envelopes3d[] Envelopes3d { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/BoundingBoxReference.cs b/RobotNet.RobotShares/VDA5050/Factsheet/BoundingBoxReference.cs new file mode 100644 index 0000000..8ffc313 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/BoundingBoxReference.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class BoundingBoxReference +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public double Z { get; set; } + public double Theta { get; set; } + +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes2d.cs b/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes2d.cs new file mode 100644 index 0000000..647dc09 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes2d.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class PolygonPoints +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } +} +public class Envelopes2d +{ + [Required] + public string Set { get; set; } + [Required] + public PolygonPoints[] PolygonPoints { get; set; } + public string Description { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes3d.cs b/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes3d.cs new file mode 100644 index 0000000..5dc5225 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/Envelopes3d.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class Envelopes3d +{ + [Required] + public string Set { get; set; } + [Required] + public string Format { get; set; } + public object Data { get; set; } + public string Url { get; set; } + public string Description { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/FactSheetMsg.cs b/RobotNet.RobotShares/VDA5050/Factsheet/FactSheetMsg.cs new file mode 100644 index 0000000..dcdfed9 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/FactSheetMsg.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class FactSheetMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } + [Required] + public string Version { get; set; } + [Required] + public string Manufacturer { get; set; } + [Required] + public string SerialNumber { get; set; } + [Required] + public TypeSpecification TypeSpecification { get; set; } + [Required] + public PhysicalParameters PhysicalParameters { get; set; } + [Required] + public ProtocolLimits ProtocolLimits { get; set; } + [Required] + public ProtocolFeatures ProtocolFeatures { get; set; } + [Required] + public AgvGeometry AgvGeometry { get; set; } + [Required] + public LoadSpecification LoadSpecification { get; set; } + //public LocalizationParameter LocalizationParameters { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/LoadDimensions.cs b/RobotNet.RobotShares/VDA5050/Factsheet/LoadDimensions.cs new file mode 100644 index 0000000..260d955 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/LoadDimensions.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class LoadDimensions +{ + [Required] + public double Length { get; set; } + [Required] + public double Width { get; set; } + public double Height { get; set; } + +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/LoadSets.cs b/RobotNet.RobotShares/VDA5050/Factsheet/LoadSets.cs new file mode 100644 index 0000000..a918c2a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/LoadSets.cs @@ -0,0 +1,26 @@ +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class LoadSets +{ + public string SetName { get; set; } + public string LoadType { get; set; } + public string[] LoadPositions { get; set; } + public BoundingBoxReference BoundingBoxReference { get; set; } + public LoadDimensions LoadDimensions { get; set; } + public double MaxWeigth { get; set; } + public double MinLoadhandlingHeight { get; set; } + public double MaxLoadhandlingHeight { get; set; } + public double MinLoadhandlingDepth { get; set; } + public double MaxLoadhandlingDepth { get; set; } + public double MinLoadhandlingTilt { get; set; } + public double MaxLoadhandlingTilt { get; set; } + public double AgvSpeedLimit { get; set; } + public double AgvAccelerationLimit { get; set; } + public double AgvDecelerationLimit { get; set; } + public double PickTime { get; set; } + public double DropTime { get; set; } + public string Description { get; set; } + +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/LoadSpecification.cs b/RobotNet.RobotShares/VDA5050/Factsheet/LoadSpecification.cs new file mode 100644 index 0000000..1a82c26 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/LoadSpecification.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class LoadSpecification +{ + public string[] LoadPositions { get; set; } + public LoadSets[] LoadSets { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/LocalizationParameter.cs b/RobotNet.RobotShares/VDA5050/Factsheet/LocalizationParameter.cs new file mode 100644 index 0000000..f24bf3a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/LocalizationParameter.cs @@ -0,0 +1,6 @@ +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class LocalizationParameter +{ + public double LocalizationParameters { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/MaxArrayLens.cs b/RobotNet.RobotShares/VDA5050/Factsheet/MaxArrayLens.cs new file mode 100644 index 0000000..94225e0 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/MaxArrayLens.cs @@ -0,0 +1,21 @@ +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class MaxArrayLens +{ + public int OrderNodes { get; set; } + public int OrderEdges { get; set; } + public int NodeActions { get; set; } + public int EdgeActions { get; set; } + public int ActionsActionsParameters { get; set; } + public int InstantActions { get; set; } + public int TrajectoryKnotVector { get; set; } + public int TrajectoryControlPoints { get; set; } + public int StateNodeStates { get; set; } + public int StateEdgeStates { get; set; } + public int StateLoads { get; set; } + public int StateActionStates { get; set; } + public int StateErrors { get; set; } + public int StateInformation { get; set; } + public int ErrorErrorReferences { get; set; } + public int InformationInfoReferences { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/MaxStringLens.cs b/RobotNet.RobotShares/VDA5050/Factsheet/MaxStringLens.cs new file mode 100644 index 0000000..36b34f1 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/MaxStringLens.cs @@ -0,0 +1,12 @@ +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class MaxStringLens +{ + public int MsgLen { get; set; } + public int TopicSerialLen { get; set; } + public int TopicElemLen { get; set; } + public int IdLen { get; set; } + public bool IdNumericalOnly { get; set; } + public int EnumLen { get; set; } + public int LoadIdLen { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/OptionalParameters.cs b/RobotNet.RobotShares/VDA5050/Factsheet/OptionalParameters.cs new file mode 100644 index 0000000..c080b5a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/OptionalParameters.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public enum Support +{ + SUPPORTED, + REQUIRED, +} +public class OptionalParameters +{ + [Required] + public string Parameter { get; set; } + [Required] + public string Support { get; set; } + public string Description { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/PhysicalParameters.cs b/RobotNet.RobotShares/VDA5050/Factsheet/PhysicalParameters.cs new file mode 100644 index 0000000..291879b --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/PhysicalParameters.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class PhysicalParameters +{ + [Required] + public double SpeedMin { get; set; } + [Required] + public double SpeedMax { get; set; } + [Required] + public double AccelerationMax { get; set; } + [Required] + public double DecelerationMax { get; set; } + public double HeightMin { get; set; } + [Required] + public double HeightMax { get; set; } + [Required] + public double Width { get; set; } + [Required] + public double Length { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolFeatures.cs b/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolFeatures.cs new file mode 100644 index 0000000..43b2ebe --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolFeatures.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public class ProtocolFeatures +{ + [Required] + public OptionalParameters[] OptionalParameters { get; set; } + [Required] + public AgvActions[] AgvActions { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolLimits.cs b/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolLimits.cs new file mode 100644 index 0000000..4be3478 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/ProtocolLimits.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable +public class ProtocolLimits +{ + [Required] + public MaxStringLens MaxStringLens { get; set; } + [Required] + public MaxArrayLens MaxArrayLens { get; set; } + [Required] + public Timing Timing { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/Timing.cs b/RobotNet.RobotShares/VDA5050/Factsheet/Timing.cs new file mode 100644 index 0000000..958c2eb --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/Timing.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +public class Timing +{ + [Required] + public double MinOrderInterval { get; set; } + [Required] + public double MinStateInterval { get; set; } + public double DefaultStateInterval { get; set; } + public double VisualizationInterval { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/TypeSpecification.cs b/RobotNet.RobotShares/VDA5050/Factsheet/TypeSpecification.cs new file mode 100644 index 0000000..0612bed --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/TypeSpecification.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public enum AgvKinematic +{ + DIFF, + OMNI, + THREEWHEEL +} +public enum AgvClass +{ + FORKLIFT, + CONVEYOR, + TUGGER, + CARRIER +} +public enum LocalizationTypes +{ + NATURAL, + REFLECTOR, + RFID, + DMC, + SPOT, + GRID, +} +public enum NavigationTypes +{ + PHYSICAL_LINDE_GUIDED, + VIRTUAL_LINE_GUIDED, + AUTONOMOUS, +} +public class TypeSpecification +{ + [Required] + public string SeriesName { get; set; } + public string SeriesDescription { get; set; } + [Required] + public string AgvKinematic { get; set; } + [Required] + public string AgvClass { get; set; } + [Required] + public double MaxLoadMass { get; set; } + [Required] + public string[] LocalizationTypes { get; set; } = []; + [Required] + public string[] NavigationTypes { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Factsheet/WheelDefinitions.cs b/RobotNet.RobotShares/VDA5050/Factsheet/WheelDefinitions.cs new file mode 100644 index 0000000..a3bd14e --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Factsheet/WheelDefinitions.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Factsheet; + +#nullable disable + +public enum WheelDefinitionsType +{ + DRIVE, + CASTER, + FIXED, + MECANUM, +} +public class WheelDefinitionsPosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } +} + +public class WheelDefinitions +{ + [Required] + public string Type { get; set; } + [Required] + public bool IsActiveDriven { get; set; } + [Required] + public bool IsActiveSteered { get; set; } + [Required] + public WheelDefinitionsPosition Position { get; set; } + [Required] + public double Diameter { get; set; } + [Required] + public double Width { get; set; } + public double CenterDisplacement { get; set; } + public string Constraints { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Battery.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Battery.cs new file mode 100644 index 0000000..0ee5e56 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Battery.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Battery +{ + public uint Battery_low { get; set; } + public uint Battery_normal { get; set; } + public uint Battery_good { get; set; } + public uint Battery_full { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/BatteryThreshold.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/BatteryThreshold.cs new file mode 100644 index 0000000..f3a0d91 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/BatteryThreshold.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public enum BatteryThreshold +{ + LOW, + NORMAL, + MIDDLE, + GOOD, + FULL, + NONE +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/CameraSafety.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/CameraSafety.cs new file mode 100644 index 0000000..bce3805 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/CameraSafety.cs @@ -0,0 +1,31 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class CameraSafety +{ + public double Pass_through_x_min { get; set; } + public double Pass_through_x_max { get; set; } + public double Pass_through_y_min { get; set; } + public double Pass_through_y_max { get; set; } + public double Pass_through_z_min { get; set; } + public double Pass_through_z_max { get; set; } + public uint Ground_seg_max_iterations { get; set; } + public double Ground_seg_distance_threshold { get; set; } + public double Warn_z1 { get; set; } + public double Protect_z1 { get; set; } + public double Warn_z2 { get; set; } + public double Protect_z2 { get; set; } + public double Warn_z3 { get; set; } + public double Protect_z3 { get; set; } + public double Warn_z4 { get; set; } + public double Protect_z4 { get; set; } + public double Warn_z5 { get; set; } + public double Protect_z5 { get; set; } + public double Warn_z6 { get; set; } + public double Protect_z6 { get; set; } + public uint Min_cluster_warn_size { get; set; } + public uint Min_cluster_protect_size { get; set; } + public uint Min_cluster_detect_size { get; set; } + public uint Min_consecutive_warn_count { get; set; } + public uint Min_consecutive_protect_count { get; set; } + public uint Min_consecutive_detect_count { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/ChargerParam.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ChargerParam.cs new file mode 100644 index 0000000..31630b2 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ChargerParam.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +#nullable disable + +public class ChargerParam +{ + public string Charger_ip { get; set; } + public uint Charger_port { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/FactsheetExtendMsg.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/FactsheetExtendMsg.cs new file mode 100644 index 0000000..ce5d154 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/FactsheetExtendMsg.cs @@ -0,0 +1,19 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +#nullable disable + +public class FactsheetExtendMsg +{ + public uint HeaderId { get; set; } + public DateTime Timestamp { get; set; } + public string Version { get; set; } + public string Manufacturer { get; set; } + public string SerialNumber { get; set; } + + public ServerParam Server_param { get; set; } + public RobotParam Robot_param { get; set; } + public Localization Localization { get; set; } + public Navigation Navigation { get; set; } + public Safety Safety { get; set; } + public ChargerParam Charger_param { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/ForkSafety.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ForkSafety.cs new file mode 100644 index 0000000..e68059a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ForkSafety.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class ForkSafety +{ + public double Muted_field_size { get; set; } + public double Protected_field_size { get; set; } + public double Warning_field_size { get; set; } + public double Detect_field_size { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Initpose.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Initpose.cs new file mode 100644 index 0000000..9ef6e24 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Initpose.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Initpose +{ + public bool Use_manual_initpose { get; set; } + public double Initpose_x { get; set; } + public double Initpose_y { get; set; } + public double Initpose_yaw { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/LineSegment.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/LineSegment.cs new file mode 100644 index 0000000..1124711 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/LineSegment.cs @@ -0,0 +1,10 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class LineSegment +{ + public double Least_thresh { get; set; } + public double Min_line_length { get; set; } + public double Predict_distance { get; set; } + public uint Seed_line_points { get; set; } + public uint Min_line_points { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Localization.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Localization.cs new file mode 100644 index 0000000..54315f7 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Localization.cs @@ -0,0 +1,14 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +#nullable disable + +public class Localization +{ + public uint Threshold_quality_loc { get; set; } + public bool Use_localization_marker { get; set; } + public bool Use_pallet_detection { get; set; } + public Initpose Initpose { get; set; } + public Xloc Xloc { get; set; } + public VlMarker Vl_marker { get; set; } + +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Motor.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Motor.cs new file mode 100644 index 0000000..c02f7b3 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Motor.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Motor +{ + public double OdomEncSteeringAngleOffset { get; set; } + public double Steering_fix_wheel_distance_x { get; set; } + public double Steering_fix_wheel_distance_y { get; set; } + public double WheelAcceleration { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Navigation.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Navigation.cs new file mode 100644 index 0000000..a788cf2 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Navigation.cs @@ -0,0 +1,11 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +#nullable disable +public class Navigation +{ + public bool Using_control_safety { get; set; } + public uint Control_rate { get; set; } + public Rotate Rotate { get; set; } + public PTA Pta { get; set; } + public PPA Ppa { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/PPA.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/PPA.cs new file mode 100644 index 0000000..e220aa8 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/PPA.cs @@ -0,0 +1,7 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class PPA +{ + public double Ppa_accuracy_goal { get; set; } + public double Ppa_distance_reduce { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/PTA.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/PTA.cs new file mode 100644 index 0000000..fcc38c4 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/PTA.cs @@ -0,0 +1,15 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class PTA +{ + public double Pta_linear_vel_max { get; set; } + public double Pta_linear_vel_min { get; set; } + public double Pta_accuracy_goal { get; set; } + public double Pta_distance_reduce { get; set; } + public double Pta_acceleration { get; set; } + public double Pta_lm_front { get; set; } + public double Pta_lm_back { get; set; } + public double Pta_phi_max { get; set; } + public double Pta_amplitude_max { get; set; } + public uint Pta_w_option { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/RobotParam.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/RobotParam.cs new file mode 100644 index 0000000..1e17033 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/RobotParam.cs @@ -0,0 +1,12 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class RobotParam +{ + public bool Use_dynamic_parameter { get; set; } // (Default: True) Declare whether to use dynamic parameters or not + public string? Ethernet_name { get; set; } // The name of Ethernet port which connects to wifi client (e.g eno1, lo, enp3s0) + public double Speed_max_backward { get; set; } // (Default: True) Declare whether to use dynamic parameters or not + public uint Num_day_logger { get; set; } // The name of Ethernet port which connects to wifi client (e.g eno1, lo, enp3s0) + + public Motor Motor { get; set; } = new(); + public Battery Battery { get; set; } = new(); +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Rotate.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Rotate.cs new file mode 100644 index 0000000..5bd15e0 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Rotate.cs @@ -0,0 +1,9 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Rotate +{ + public double Angular_vel_max { get; set; } + public double Angular_vel_min { get; set; } + public double Acceleration_rotate { get; set; } + public double Tolerances_rotate { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Safety.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Safety.cs new file mode 100644 index 0000000..bef650f --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Safety.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Safety +{ + public bool Use_camera_safety { get; set; } + public CameraSafety Camera_safety { get; set; } = new(); + public ForkSafety Fork_safety { get; set; } = new(); +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/ServerParam.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ServerParam.cs new file mode 100644 index 0000000..8e20248 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/ServerParam.cs @@ -0,0 +1,14 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +#nullable disable + +public class ServerParam +{ + public string Server_ip { get; set; } + public string Server_port { get; set; } + public string Keepalive { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Client_protocol { get; set; } + public string Client_id { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/VlMarker.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/VlMarker.cs new file mode 100644 index 0000000..da5a22c --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/VlMarker.cs @@ -0,0 +1,21 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class VlMarker +{ + public bool Use_odometry { get; set; } + public uint V_angle { get; set; } + public double Length_v { get; set; } + public double Length_l { get; set; } + public double X_laser { get; set; } + public double Y_laser { get; set; } + public bool Flip_laser { get; set; } + public double Rotate_laser { get; set; } + public uint Frequence_control { get; set; } + public double Angle_min { get; set; } + public double Angle_max { get; set; } + public double Max_init_x { get; set; } + public double Max_init_y { get; set; } + public double Max_init_yaw { get; set; } + + public LineSegment Line_segment { get; set; } = new(); +} diff --git a/RobotNet.RobotShares/VDA5050/FactsheetExtend/Xloc.cs b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Xloc.cs new file mode 100644 index 0000000..7307051 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/FactsheetExtend/Xloc.cs @@ -0,0 +1,16 @@ +namespace RobotNet.RobotShares.VDA5050.FactsheetExtend; + +public class Xloc +{ + public double Front_vls_width { get; set; } + public double Front_vls_pose_x { get; set; } + public double Front_vls_pose_y { get; set; } + public double Front_vls_pose_yaw { get; set; } + public uint Front_vls_source_id { get; set; } + + public double Rear_vls_width { get; set; } + public double Rear_vls_pose_x { get; set; } + public double Rear_vls_pose_y { get; set; } + public double Rear_vls_pose_yaw { get; set; } + public uint Rear_vls_source_id { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/InstantAction/ActionParameter.cs b/RobotNet.RobotShares/VDA5050/InstantAction/ActionParameter.cs new file mode 100644 index 0000000..244ff94 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/InstantAction/ActionParameter.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.InstantAction; + +#nullable disable +public class ActionParameter +{ + [Required] + public string Key { get; set; } = ""; + [Required] + public string Value { get; set; } = ""; +} diff --git a/RobotNet.RobotShares/VDA5050/InstantAction/Actions.cs b/RobotNet.RobotShares/VDA5050/InstantAction/Actions.cs new file mode 100644 index 0000000..f9a5b54 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/InstantAction/Actions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.InstantAction; + +#nullable disable + +public enum BlockingType +{ + NONE, + SOFT, + HARD +} +public class Action +{ + [Required] + public string ActionType { get; set; } = ""; + [Required] + public string ActionId { get; set; } = ""; + public string ActionDescription { get; set; } = ""; + [Required] + public string BlockingType { get; set; } = ""; + public ActionParameter[] ActionParameters { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/InstantAction/InstantActionsMsg.cs b/RobotNet.RobotShares/VDA5050/InstantAction/InstantActionsMsg.cs new file mode 100644 index 0000000..0ece680 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/InstantAction/InstantActionsMsg.cs @@ -0,0 +1,13 @@ +namespace RobotNet.RobotShares.VDA5050.InstantAction; + +#nullable disable + +public class InstantActionsMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } = ""; + public string Version { get; set; } = ""; + public string Manufacturer { get; set; } = ""; + public string SerialNumber { get; set; } = ""; + public Action[] Actions { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/Corridor.cs b/RobotNet.RobotShares/VDA5050/Order/Corridor.cs new file mode 100644 index 0000000..ff39050 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/Corridor.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +public enum CorridorRefPoint +{ + KINEMATICCENTER, + CONTOUR +} + +public class Corridor +{ + [Required] + public double LeftWidth { get; set; } + [Required] + public double RightWidth { get; set; } + public string CorridorRefPoint { get; set; } = ""; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/Edge.cs b/RobotNet.RobotShares/VDA5050/Order/Edge.cs new file mode 100644 index 0000000..c721eb6 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/Edge.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class Edge +{ + [Required] + public string EdgeId { get; set; } = ""; + [Required] + public int SequenceId { get; set; } + public string EdgeDescription { get; set; } = ""; + [Required] + public bool Released { get; set; } + [Required] + public string StartNodeId { get; set; } = ""; + [Required] + public string EndNodeId { get; set; } = ""; + public double MaxSpeed { get; set; } + public double MaxHeight { get; set; } + public double MinHeight { get; set; } + public double Orientation { get; set; } + public string OrientationType { get; set; } = ""; + public string Direction { get; set; } = ""; + public bool RotationAllowed { get; set; } + public double MaxRotationSpeed { get; set; } + public double Length { get; set; } + public Trajectory Trajectory { get; set; } + public Corridor Corridor { get; set; } = new(); + [Required] + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/EdgeLog.cs b/RobotNet.RobotShares/VDA5050/Order/EdgeLog.cs new file mode 100644 index 0000000..c67b05d --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/EdgeLog.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable +public class EdgeLog +{ + + [Required] + public string EdgeId { get; set; } = ""; + public string EdgeDescription { get; set; } = ""; + [Required] + public string StartNodeId { get; set; } = ""; + [Required] + public string EndNodeId { get; set; } = ""; + public Trajectory Trajectory { get; set; } = new(); + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/Node.cs b/RobotNet.RobotShares/VDA5050/Order/Node.cs new file mode 100644 index 0000000..0dc0485 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/Node.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class Node +{ + [Required] + public string NodeId { get; set; } = ""; + [Required] + public int SequenceId { get; set; } + public string NodeDescription { get; set; } = ""; + [Required] + public bool Released { get; set; } + public NodePosition NodePosition { get; set; } + [Required] + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/NodeLog.cs b/RobotNet.RobotShares/VDA5050/Order/NodeLog.cs new file mode 100644 index 0000000..194137a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/NodeLog.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class NodeLog +{ + [Required] + public string NodeId { get; set; } = ""; + public string NodeDescription { get; set; } = ""; + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public double Theta { get; set; } + public InstantAction.Action[] Actions { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/NodePosition.cs b/RobotNet.RobotShares/VDA5050/Order/NodePosition.cs new file mode 100644 index 0000000..d255cee --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/NodePosition.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class NodePosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } + public double AllowedDeviationXY { get; set; } + public double AllowedDeviationTheta { get; set; } + [Required] + public string MapId { get; set; } = ""; + public string MapDescription { get; set; } = ""; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/OrderLog.cs b/RobotNet.RobotShares/VDA5050/Order/OrderLog.cs new file mode 100644 index 0000000..ee4842b --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/OrderLog.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class OrderLog +{ + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string OrderId { get; set; } = ""; + [Required] + public int OrderUpdateId { get; set; } + [Required] + public NodeLog[] Nodes { get; set; } = []; + [Required] + public EdgeLog[] Edges { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/Order/OrderMsg.cs b/RobotNet.RobotShares/VDA5050/Order/OrderMsg.cs new file mode 100644 index 0000000..ea9d285 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/OrderMsg.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class OrderMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } = ""; + [Required] + public string Version { get; set; } = ""; + [Required] + public string Manufacturer { get; set; } = ""; + [Required] + public string SerialNumber { get; set; } = ""; + [Required] + public string OrderId { get; set; } = ""; + [Required] + public int OrderUpdateId { get; set; } + public string ZoneSetId { get; set; } = ""; + [Required] + public Node[] Nodes { get; set; } = []; + [Required] + public Edge[] Edges { get; set; } = []; + +} diff --git a/RobotNet.RobotShares/VDA5050/Order/Trajectory.cs b/RobotNet.RobotShares/VDA5050/Order/Trajectory.cs new file mode 100644 index 0000000..f2980d9 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Order/Trajectory.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Order; + +#nullable disable + +public class ControlPoint +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Weight { get; set; } +} +public class Trajectory +{ + [Required] + public int Degree { get; set; } + [Required] + public double[] KnotVector { get; set; } = []; + [Required] + public ControlPoint[] ControlPoints { get; set; } = []; +} diff --git a/RobotNet.RobotShares/VDA5050/State/ActionState.cs b/RobotNet.RobotShares/VDA5050/State/ActionState.cs new file mode 100644 index 0000000..b2ffb91 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/ActionState.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public enum ActionStatus +{ + WAITING, + INITIALIZING, + RUNNING, + PAUSED, + FINISHED, + FAILED, +} +public class ActionState +{ + public string ActionType { get; set; } + [Required] + public string ActionId { get; set; } + public string ActionDescription { get; set; } + [Required] + public string ActionStatus { get; set; } + public string ResultDescription { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/BatteryState.cs b/RobotNet.RobotShares/VDA5050/State/BatteryState.cs new file mode 100644 index 0000000..4756b1e --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/BatteryState.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +public class BatteryState +{ + [Required] + public double BatteryCharge { get; set; } + public double BatteryVoltage { get; set; } + [Range(0, 100, ErrorMessage = "Value for BatteryHealth must be between 0 and 100.")] + public double BatteryHealth { get; set; } + [Required] + public bool Charging { get; set; } + public double Reach { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/EdgeState.cs b/RobotNet.RobotShares/VDA5050/State/EdgeState.cs new file mode 100644 index 0000000..a58a86a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/EdgeState.cs @@ -0,0 +1,19 @@ +using RobotNet.RobotShares.VDA5050.Order; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public class EdgeState +{ + + [Required] + public string EdgeId { get; set; } + [Required] + public int SequenceId { get; set; } + public string EdgeDescription { get; set; } + [Required] + public bool Released { get; set; } + public Trajectory Trajectory { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/Error.cs b/RobotNet.RobotShares/VDA5050/State/Error.cs new file mode 100644 index 0000000..dfc301a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/Error.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public enum ErrorLevel +{ + NONE, + WARNING, + FATAL +} +public class ErrorReferences +{ + [Required] + public string ReferenceKey { get; set; } + [Required] + public string ReferenceValue { get; set; } +} +public class Error +{ + [Required] + public string ErrorType { get; set; } + public ErrorReferences[] ErrorReferences { get; set; } + public string ErrorDescription { get; set; } + public string ErrorHint { get; set; } + [Required] + public string ErrorLevel { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/Information.cs b/RobotNet.RobotShares/VDA5050/State/Information.cs new file mode 100644 index 0000000..f00c035 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/Information.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public enum InfoLevel +{ + INFO, + DEBUG +} +public class InfomationReferences +{ + [Required] + public string ReferenceKey { get; set; } + [Required] + public string ReferenceValue { get; set; } +} +public class Information +{ + [Required] + public string InfoType { get; set; } + public InfomationReferences[] InfoReferences { get; set; } + public string InfoDescription { get; set; } + [Required] + public string InfoLevel { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/Load.cs b/RobotNet.RobotShares/VDA5050/State/Load.cs new file mode 100644 index 0000000..9b15851 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/Load.cs @@ -0,0 +1,16 @@ +using RobotNet.RobotShares.VDA5050.Factsheet; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public class Load +{ + public string LoadId { get; set; } + public string LoadType { get; set; } + public string LoadPosition { get; set; } + public BoundingBoxReference BoundingBoxReference { get; set; } + public LoadDimensions LoadDimensions { get; set; } + public double Weight { get; set; } + +} diff --git a/RobotNet.RobotShares/VDA5050/State/Map.cs b/RobotNet.RobotShares/VDA5050/State/Map.cs new file mode 100644 index 0000000..08e1ac0 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/Map.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +public enum MapStatus +{ + Enabled, + Disabled, +} + +public class Map +{ + [Required] + public string MapId { get; set; } = string.Empty; + [Required] + public string MapVersion { get; set; } = string.Empty; + public string MapDescription { get; set; } = string.Empty; + [Required] + public string MapStatus { get; set; } = string.Empty; +} diff --git a/RobotNet.RobotShares/VDA5050/State/NodeState.cs b/RobotNet.RobotShares/VDA5050/State/NodeState.cs new file mode 100644 index 0000000..83c0418 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/NodeState.cs @@ -0,0 +1,30 @@ +using RobotNet.RobotShares.VDA5050.Order; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public class NodeState +{ + [Required] + public string NodeId { get; set; } + [Required] + public int SequenceId { get; set; } + public string NodeDescription { get; set; } + [Required] + public bool Released { get; set; } + public NodePosition NodePosition { get; set; } +} + +public class NodePosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + public double Theta { get; set; } + [Required] + public string MapId { get; set; } = ""; + +} \ No newline at end of file diff --git a/RobotNet.RobotShares/VDA5050/State/SafetyState.cs b/RobotNet.RobotShares/VDA5050/State/SafetyState.cs new file mode 100644 index 0000000..41c47c3 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/SafetyState.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +public enum EStop +{ + AUTOACK, + MANUAL, + REMOTE, + NONE, +} + +public class SafetyState +{ + [Required] + public string? EStop { get; set; } + + [Required] + public bool FieldViolation { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/State/StateMsg.cs b/RobotNet.RobotShares/VDA5050/State/StateMsg.cs new file mode 100644 index 0000000..51de0fb --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/State/StateMsg.cs @@ -0,0 +1,61 @@ +using RobotNet.RobotShares.VDA5050.Visualization; +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.State; + +#nullable disable + +public enum OperatingMode +{ + AUTOMATIC, + SEMIAUTOMATIC, + MANUAL, + SERVICE, + TEACHIN, +} +public class StateMsg +{ + [Required] + public uint HeaderId { get; set; } + [Required] + public string Timestamp { get; set; } + [Required] + public string Version { get; set; } + [Required] + public string Manufacturer { get; set; } + [Required] + public string SerialNumber { get; set; } + public Map[] Maps { get; set; } = []; + [Required] + public string OrderId { get; set; } + [Required] + public int OrderUpdateId { get; set; } + public string ZoneSetId { get; set; } + [Required] + public string LastNodeId { get; set; } + [Required] + public int LastNodeSequenceId { get; set; } + [Required] + public bool Driving { get; set; } + public bool Paused { get; set; } + public bool NewBaseRequest { get; set; } + public double DistanceSinceLastNode { get; set; } + [Required] + public string OperatingMode { get; set; } + [Required] + public NodeState[] NodeStates { get; set; } = []; + [Required] + public EdgeState[] EdgeStates { get; set; } = []; + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity Velocity { get; set; } = new(); + public Load[] Loads { get; set; } = []; + [Required] + public ActionState[] ActionStates { get; set; } = []; + [Required] + public BatteryState BatteryState { get; set; } = new(); + [Required] + public Error[] Errors { get; set; } = []; + public Information[] Information { get; set; } = []; + [Required] + public SafetyState SafetyState { get; set; } = new(); +} diff --git a/RobotNet.RobotShares/VDA5050/Type/ActionType.cs b/RobotNet.RobotShares/VDA5050/Type/ActionType.cs new file mode 100644 index 0000000..eb16835 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Type/ActionType.cs @@ -0,0 +1,35 @@ +namespace RobotNet.RobotShares.VDA5050.Type; + +public enum ActionType +{ + startPause, // No actionParameters + stopPause, // No actionParameters + startCharging, // ActionParameters {CHARGER_IP, CHARGER_PORT, X, Y, THETA} + stopCharging, // ActionParameters {X, Y, THETA} + initPosition, // ActionParameters {X, Y, THETA, MAP_ID, LAST_NODE_ID} + stateRequest, // No actionParameters + logReport, // No actionParameters + pick, // No actionParameters + drop, // No actionParameters + detectObject, // No actionParameters + finePositioning, // ActionParameters {X, Y, THETA, MAP_ID, LAST_NODE_ID} + waitForTrigger, // No actionParameters + cancelOrder, // No actionParameters + factsheetRequest, // No actionParameters + + setDynparam, // ActionParameters {PARAM_NAME, PARAM_TYPE, PARAM_VALUE} + saveDynparamRuntime, // No actionParameters + loadDynparamRuntime, // No actionParameters + loadDynparamDefault, // No actionParameters + getDynparamRuntime, // No actionParameters + + controlLight, // ActionParameters {LIGHT_TYPE, CONTROL_TYPE} + controlFan, // ActionParameters {CONTROL_TYPE} + controlSpeaker, // ActionParameters {SONG_NUMBER, CONTROL_TYPE} + controlSafetyField, // ActionParameters {FIELD_TYPE, CONTROL_TYPE} + startInPallet, // ActionParameters {X, Y, THETA} + startOutPallet, // ActionParameters {X, Y, THETA} + + rotate, // ActionParameters {THETA} + setMap, // ActionParameter {MAP_ID} +} diff --git a/RobotNet.RobotShares/VDA5050/Type/InformationType.cs b/RobotNet.RobotShares/VDA5050/Type/InformationType.cs new file mode 100644 index 0000000..c26fe00 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Type/InformationType.cs @@ -0,0 +1,23 @@ +namespace RobotNet.RobotShares.VDA5050.Type; + +public enum InformationType +{ + connection_server, + order, + control_navigation, + robot_general +} + +public enum InformationReferencesKey +{ + count_retry, + robot_area, + robot_state, +} + +public enum InformationMMGArea +{ + CHARGING, + WORKING, + HOME, +} \ No newline at end of file diff --git a/RobotNet.RobotShares/VDA5050/Type/LoadType.cs b/RobotNet.RobotShares/VDA5050/Type/LoadType.cs new file mode 100644 index 0000000..7202916 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Type/LoadType.cs @@ -0,0 +1,7 @@ +namespace RobotNet.RobotShares.VDA5050.Type; + +public enum LoadType +{ + PALLET, + GOODS +} diff --git a/RobotNet.RobotShares/VDA5050/Type/ManualActionType.cs b/RobotNet.RobotShares/VDA5050/Type/ManualActionType.cs new file mode 100644 index 0000000..efe7c09 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Type/ManualActionType.cs @@ -0,0 +1,17 @@ +namespace RobotNet.RobotShares.VDA5050.Type; + +public enum ManualActionType +{ + RELEASE, + GO_HOME, + GO_CHARGING, + STOP_CHARGING, + MOVE_TO_NODE, + PICK_PALLET, + DROP_PALLET, + FACTSHEET_REQUEST, + SET_MAP, + PICK, + DROP, + CHARGING +} diff --git a/RobotNet.RobotShares/VDA5050/VDA5050Helper.cs b/RobotNet.RobotShares/VDA5050/VDA5050Helper.cs new file mode 100644 index 0000000..20e893a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/VDA5050Helper.cs @@ -0,0 +1,29 @@ +using RobotNet.RobotShares.VDA5050.State; +using System.Text; + +namespace RobotNet.RobotShares.VDA5050; + +public class VDA5050Helper +{ + public static string ConvertErrorDetail(IEnumerable errors) + { + string errorsType = ""; + foreach (var error in errors) + { + if (error == errors.Last()) errorsType += $"{error.ErrorType}"; + else errorsType += $"{error.ErrorType}, "; + } + StringBuilder errorStr = new($"Robot có lỗi: [{errorsType}]\n"); + foreach (var error in errors) + { + string errorDes = $"- {error.ErrorType}: {error.ErrorDescription}\n"; + errorStr.Append(errorDes.PadLeft(errorDes.Length + 3)); + foreach (var refer in error.ErrorReferences) + { + string errorRefer = $"+ {refer.ReferenceKey}: {refer.ReferenceValue}\n"; + errorStr.Append(errorRefer.PadLeft(errorRefer.Length + 9)); + } + } + return !errors.Any() ? "" : errorStr.ToString(); + } +} diff --git a/RobotNet.RobotShares/VDA5050/VDA5050Setting.cs b/RobotNet.RobotShares/VDA5050/VDA5050Setting.cs new file mode 100644 index 0000000..d48fa35 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/VDA5050Setting.cs @@ -0,0 +1,19 @@ +namespace RobotNet.RobotShares.VDA5050; + +#nullable disable + +public class VDA5050Setting +{ + public bool ServerEnable { get; set; } + public string HostServer { get; set; } + public int Port { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + public string Manufacturer { get; set; } + public string Version { get; set; } + public int Repeat { get; set; } + public int ConnectionTimeoutSeconds { get; set; } + public int ConnectionBacklog { set; get; } + public int KeepAliveInterval { get; set; } + public int CheckingRobotMsgTimout { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/VDA5050Topic.cs b/RobotNet.RobotShares/VDA5050/VDA5050Topic.cs new file mode 100644 index 0000000..ccee10a --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/VDA5050Topic.cs @@ -0,0 +1,12 @@ +namespace RobotNet.RobotShares.VDA5050; + +public static class VDA5050Topic +{ + public const string Connection = "connection"; + public const string Order = "order"; + public const string InstantActions = "instantActions"; + public const string State = "state"; + public const string Visualization = "visualization"; + public const string Factsheet = "factsheet"; + public const string FactsheetExtend = "factsheetExtend"; +} diff --git a/RobotNet.RobotShares/VDA5050/Visualization/AgvPosition.cs b/RobotNet.RobotShares/VDA5050/Visualization/AgvPosition.cs new file mode 100644 index 0000000..3f8715b --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Visualization/AgvPosition.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.RobotShares.VDA5050.Visualization; + +#nullable disable + +public class AgvPosition +{ + [Required] + public double X { get; set; } + [Required] + public double Y { get; set; } + [Required] + public string MapId { get; set; } + [Required] + public double Theta { get; set; } + [Required] + public bool PositionInitialized { get; set; } + public double LocalizationScore { get; set; } + public double DeviationRange { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Visualization/Velocity.cs b/RobotNet.RobotShares/VDA5050/Visualization/Velocity.cs new file mode 100644 index 0000000..9c9d5b0 --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Visualization/Velocity.cs @@ -0,0 +1,8 @@ +namespace RobotNet.RobotShares.VDA5050.Visualization; + +public class Velocity +{ + public double Vx { get; set; } + public double Vy { get; set; } + public double Omega { get; set; } +} diff --git a/RobotNet.RobotShares/VDA5050/Visualization/Visualizationmsg.cs b/RobotNet.RobotShares/VDA5050/Visualization/Visualizationmsg.cs new file mode 100644 index 0000000..79e89ff --- /dev/null +++ b/RobotNet.RobotShares/VDA5050/Visualization/Visualizationmsg.cs @@ -0,0 +1,16 @@ +namespace RobotNet.RobotShares.VDA5050.Visualization; + +#nullable disable + +public class VisualizationMsg +{ + public uint HeaderId { get; set; } + public string Timestamp { get; set; } + public string Version { get; set; } + public string Manufacturer { get; set; } + public string SerialNumber { get; set; } + public string MapId { get; set; } + public string MapDescription { get; set; } + public AgvPosition AgvPosition { get; set; } = new(); + public Velocity Velocity { get; set; } = new(); +} diff --git a/RobotNet.Script.Expressions/ElementProperties.cs b/RobotNet.Script.Expressions/ElementProperties.cs new file mode 100644 index 0000000..e21112a --- /dev/null +++ b/RobotNet.Script.Expressions/ElementProperties.cs @@ -0,0 +1,49 @@ +namespace RobotNet.Script.Expressions; + +/// +/// Quản lý các thuộc tính của một phần tử trong bản đồ. +/// +public record ElementProperties +{ + /// + /// Gets or sets a value indicating whether the resource is currently open. + /// + public bool IsOpen { get; set; } + + /// + /// Gets a dictionary that maps string keys to boolean values. + /// + public IDictionary Bool { get; } + + /// + /// Gets a dictionary that maps string keys to double values. + /// + public IDictionary Double { get; } + + /// + /// Gets a dictionary that maps string keys to integer values. + /// + public IDictionary Int { get; } + + /// + /// Gets a dictionary that maps string keys to string values. + /// + public IDictionary String { get; } + + /// + /// + /// + /// + /// + /// + /// + /// + public ElementProperties(bool isOpen, IDictionary dbool, IDictionary ddouble, IDictionary dint, IDictionary dstring) + { + IsOpen = isOpen; + Bool = dbool; + Double = ddouble; + Int = dint; + String = dstring; + } +} \ No newline at end of file diff --git a/RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj b/RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj new file mode 100644 index 0000000..8880c42 --- /dev/null +++ b/RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj @@ -0,0 +1,11 @@ + + + + net9.0 + enable + enable + True + True + + + diff --git a/RobotNet.Script.Expressions/RobotState.cs b/RobotNet.Script.Expressions/RobotState.cs new file mode 100644 index 0000000..4c7c263 --- /dev/null +++ b/RobotNet.Script.Expressions/RobotState.cs @@ -0,0 +1,14 @@ +namespace RobotNet.Script.Expressions; + + +/// +/// Trạng thái hiện tại của robot, bao gồm thông tin về vị trí, trạng thái hoạt động, pin, v.v. +/// +/// +/// +/// +/// +/// +/// +/// +public record RobotState(bool IsReady, double Voltage, bool IsLoading, bool IsCharging, double X, double Y, double Theta); diff --git a/RobotNet.Script.Shares/CreateModel.cs b/RobotNet.Script.Shares/CreateModel.cs new file mode 100644 index 0000000..ca6fffd --- /dev/null +++ b/RobotNet.Script.Shares/CreateModel.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record CreateModel(string Path, string Name); diff --git a/RobotNet.Script.Shares/Dashboard/DailyMissionDto.cs b/RobotNet.Script.Shares/Dashboard/DailyMissionDto.cs new file mode 100644 index 0000000..620bc46 --- /dev/null +++ b/RobotNet.Script.Shares/Dashboard/DailyMissionDto.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.Script.Shares.Dashboard; + +public class DailyMissionDto +{ + [Range(0, int.MaxValue)] + public int Completed { get; set; } + [Range(0, int.MaxValue)] + public int Error { get; set; } + [Range(0, int.MaxValue)] + public int Total { get; set; } + [Range(0, int.MaxValue)] + public int CompletedRate { get; set; } + [Range(0, double.MaxValue)] + public double TaktTimeMin { get; set; } + [Range(0, double.MaxValue)] + public double TaktTimeAverage { get; set; } + [Range(0, double.MaxValue)] + public double TaktTimeMax { get; set; } + [Range(0, int.MaxValue)] + public int RobotOnline { get; set; } +} diff --git a/RobotNet.Script.Shares/Dashboard/DailyPerformanceDto.cs b/RobotNet.Script.Shares/Dashboard/DailyPerformanceDto.cs new file mode 100644 index 0000000..7e90ada --- /dev/null +++ b/RobotNet.Script.Shares/Dashboard/DailyPerformanceDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.Script.Shares.Dashboard; + +public class DailyPerformanceDto +{ + public string Label { get; set; } = string.Empty; + [Range(0, int.MaxValue)] + public int Completed { get; set; } + [Range(0, int.MaxValue)] + public int Error { get; set; } + [Range(0, int.MaxValue)] + public int Other { get; set; } +} diff --git a/RobotNet.Script.Shares/Dashboard/DashboardDto.cs b/RobotNet.Script.Shares/Dashboard/DashboardDto.cs new file mode 100644 index 0000000..bedaa4b --- /dev/null +++ b/RobotNet.Script.Shares/Dashboard/DashboardDto.cs @@ -0,0 +1,11 @@ +namespace RobotNet.Script.Shares.Dashboard; + +public class DashboardDto +{ + public string TimeUpdate { get; set; } = DateTime.Now.ToString("dd/MM/yyy HH:mm:ss"); + public DailyMissionDto DailyMission { get; set; } = new (); + public DailyPerformanceDto TodayPerformance { get; set; } = new(); + public DailyPerformanceDto ThisWeekPerformance { get; set; } = new(); + public TaktTimeMissionDto[] TaktTimeMissions { get; set; } = []; + public DailyPerformanceDto[] TotalMissionPerformance { get; set; } = []; +} diff --git a/RobotNet.Script.Shares/Dashboard/TaktTimeMissionDto.cs b/RobotNet.Script.Shares/Dashboard/TaktTimeMissionDto.cs new file mode 100644 index 0000000..ffaff52 --- /dev/null +++ b/RobotNet.Script.Shares/Dashboard/TaktTimeMissionDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace RobotNet.Script.Shares.Dashboard; + +public class TaktTimeMissionDto +{ + public string Label { get; set; } = string.Empty; + [Range(0, double.MaxValue)] + public double Min { get; set; } + [Range(0, double.MaxValue)] + public double Max { get; set; } + [Range(0, double.MaxValue)] + public double Average { get; set; } +} diff --git a/RobotNet.Script.Shares/IRobotNetGlobals.cs b/RobotNet.Script.Shares/IRobotNetGlobals.cs new file mode 100644 index 0000000..d5e2145 --- /dev/null +++ b/RobotNet.Script.Shares/IRobotNetGlobals.cs @@ -0,0 +1,16 @@ +namespace RobotNet.Script.Shares; + +public interface IRobotNetGlobals +{ + Guid CurrentMissionId { get; } + RobotNet.Script.ILogger Logger { get; } + RobotNet.Script.IRobotManager RobotManager { get; } + RobotNet.Script.IMapManager MapManager { get; } + RobotNet.Script.IUnixDevice UnixDevice { get; } + RobotNet.Script.IConnectionManager ConnectionManager { get; } + Task CreateMission(string name); + Task CreateMission(string name, params object[] parameters); + bool CancelMission(Guid missionId, string reason); + bool EnableTask(string name); + bool DisableTask(string name); +} diff --git a/RobotNet.Script.Shares/InstanceMissionCreateModel.cs b/RobotNet.Script.Shares/InstanceMissionCreateModel.cs new file mode 100644 index 0000000..cf61a3f --- /dev/null +++ b/RobotNet.Script.Shares/InstanceMissionCreateModel.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record InstanceMissionCreateModel(string Name, IDictionary Parameters); diff --git a/RobotNet.Script.Shares/InstanceMissionDto.cs b/RobotNet.Script.Shares/InstanceMissionDto.cs new file mode 100644 index 0000000..1ab7927 --- /dev/null +++ b/RobotNet.Script.Shares/InstanceMissionDto.cs @@ -0,0 +1,11 @@ +namespace RobotNet.Script.Shares; + +public class InstanceMissionDto +{ + public Guid Id { get; set; } + public string MissionName { get; set; } = ""; + public string Parameters { get; set; } = "{}"; + public DateTime CreatedAt { get; set; } + public MissionStatus Status { get; set; } + public string? Log { get; set; } +} diff --git a/RobotNet.Script.Shares/MissionStatus.cs b/RobotNet.Script.Shares/MissionStatus.cs new file mode 100644 index 0000000..48aff10 --- /dev/null +++ b/RobotNet.Script.Shares/MissionStatus.cs @@ -0,0 +1,14 @@ +namespace RobotNet.Script.Shares; + +public enum MissionStatus +{ + Idle = 0, + Running, + Canceling, + Pausing, + Paused, + Resuming, + Canceled, + Completed, + Error, +} diff --git a/RobotNet.Script.Shares/ProcessorRequest.cs b/RobotNet.Script.Shares/ProcessorRequest.cs new file mode 100644 index 0000000..3c3b66d --- /dev/null +++ b/RobotNet.Script.Shares/ProcessorRequest.cs @@ -0,0 +1,29 @@ +namespace RobotNet.Script.Shares; + +public enum ProcessorRequest +{ + /// + /// + /// + None, + + /// + /// + /// + Build, + + /// + /// + /// + Run, + + /// + /// + /// + Stop, + + /// + /// + /// + Reset, +} diff --git a/RobotNet.Script.Shares/ProcessorState.cs b/RobotNet.Script.Shares/ProcessorState.cs new file mode 100644 index 0000000..52417f3 --- /dev/null +++ b/RobotNet.Script.Shares/ProcessorState.cs @@ -0,0 +1,44 @@ +namespace RobotNet.Script.Shares; + +public enum ProcessorState +{ + /// + /// The processor is idle and not currently processing any scripts. + /// + Idle, + + /// + /// + /// + Building, + + /// + /// + /// + Ready, + + /// + /// + /// + Starting, + + /// + /// The processor is currently running a script. + /// + Running, + + /// + /// + /// + Stopping, + + /// + /// The processor encountered an error while running a script. + /// + Error, + + /// + /// + /// + BuildError, +} diff --git a/RobotNet.Script.Shares/RenameModel.cs b/RobotNet.Script.Shares/RenameModel.cs new file mode 100644 index 0000000..1a2a1e7 --- /dev/null +++ b/RobotNet.Script.Shares/RenameModel.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record RenameModel(string Path, string NewName); diff --git a/RobotNet.Script.Shares/RobotNet.Script.Shares.csproj b/RobotNet.Script.Shares/RobotNet.Script.Shares.csproj new file mode 100644 index 0000000..59f29f5 --- /dev/null +++ b/RobotNet.Script.Shares/RobotNet.Script.Shares.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/RobotNet.Script.Shares/ScriptExtensions.cs b/RobotNet.Script.Shares/ScriptExtensions.cs new file mode 100644 index 0000000..7ad1022 --- /dev/null +++ b/RobotNet.Script.Shares/ScriptExtensions.cs @@ -0,0 +1,93 @@ +using System.Reflection; + +namespace RobotNet.Script.Shares; + +public static class ScriptExtensions +{ + private static readonly string PreCode; + + static ScriptExtensions() + { + var preCode = ""; + + var glovalType = typeof(IRobotNetGlobals); + + var properties = glovalType.GetProperties(); + foreach (var property in properties) + { + preCode = string.Join(Environment.NewLine, preCode, ToDeclarationString(property)); + } + + + var fields = glovalType.GetFields(); + foreach (var field in fields) + { + preCode = string.Join(Environment.NewLine, preCode, ToDeclarationString(field)); + } + + var methods = glovalType.GetMethods(); + foreach (var method in methods) + { + if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue; + + preCode = string.Join(Environment.NewLine, preCode, ToMethodString(method)); + } + + PreCode = preCode; + } + + private static string ToDeclarationString(PropertyInfo property) + { + if (property.CanRead) + { + return $"{property.PropertyType.FullName} {property.Name} {{ get; {(property.CanWrite ? "set; " : "")}}}"; + } + else if (property.CanWrite) + { + return $"{property.PropertyType.FullName} {property.Name} {{ set => throw new System.NotImplementedException(); }}"; + } + else + { + return ""; + } + } + + private static string ToDeclarationString(FieldInfo field) + { + return $"{field.FieldType.FullName} {field.Name};"; + } + + private static string ToMethodString(MethodInfo method) + { + return $"{ToTypeString(method.ReturnType)} {method.Name}({string.Join(',', method.GetParameters().Select(ToParameterString))}) => throw new System.NotImplementedException();"; + } + + private static string ToParameterString(ParameterInfo parameter) + { + var defaultValue = ""; + if (parameter.HasDefaultValue) + { + if (parameter.DefaultValue != null) + { + if (parameter.ParameterType.IsEnum) + { + defaultValue = $" = {parameter.ParameterType.FullName}.{parameter.DefaultValue}"; + } + else + { + defaultValue = $" = {parameter.DefaultValue}"; + } + } + else + { + defaultValue = " = null"; + } + } + return $"{parameter.ParameterType.FullName} {parameter.Name}{defaultValue}"; + } + + private static string ToTypeString(Type type) + { + return type == typeof(void) ? "void" : type.FullName ?? type.Name; + } +} diff --git a/RobotNet.Script.Shares/ScriptFile.cs b/RobotNet.Script.Shares/ScriptFile.cs new file mode 100644 index 0000000..24db39e --- /dev/null +++ b/RobotNet.Script.Shares/ScriptFile.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record ScriptFile(string Name, string Code); diff --git a/RobotNet.Script.Shares/ScriptFolder.cs b/RobotNet.Script.Shares/ScriptFolder.cs new file mode 100644 index 0000000..2c776d9 --- /dev/null +++ b/RobotNet.Script.Shares/ScriptFolder.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record ScriptFolder(string Name, IEnumerable Folders, IEnumerable Files); diff --git a/RobotNet.Script.Shares/ScriptMissionDto.cs b/RobotNet.Script.Shares/ScriptMissionDto.cs new file mode 100644 index 0000000..870ceb1 --- /dev/null +++ b/RobotNet.Script.Shares/ScriptMissionDto.cs @@ -0,0 +1,4 @@ +namespace RobotNet.Script.Shares; + +public record ScriptMissionParameterDto(string Name, string Type, string? Default); +public record ScriptMissionDto (string Name, IEnumerable Parameters, string Code); diff --git a/RobotNet.Script.Shares/ScriptTaskDto.cs b/RobotNet.Script.Shares/ScriptTaskDto.cs new file mode 100644 index 0000000..96f7c85 --- /dev/null +++ b/RobotNet.Script.Shares/ScriptTaskDto.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record ScriptTaskDto(string Name, int Interval, string Code); diff --git a/RobotNet.Script.Shares/ScriptVariableDto.cs b/RobotNet.Script.Shares/ScriptVariableDto.cs new file mode 100644 index 0000000..7f97b48 --- /dev/null +++ b/RobotNet.Script.Shares/ScriptVariableDto.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record ScriptVariableDto(string Name, string TypeName, string Value); diff --git a/RobotNet.Script.Shares/UpdateModel.cs b/RobotNet.Script.Shares/UpdateModel.cs new file mode 100644 index 0000000..ea2f93b --- /dev/null +++ b/RobotNet.Script.Shares/UpdateModel.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record UpdateModel(string Path, string Code); diff --git a/RobotNet.Script.Shares/UpdateVariableModel.cs b/RobotNet.Script.Shares/UpdateVariableModel.cs new file mode 100644 index 0000000..81c1c88 --- /dev/null +++ b/RobotNet.Script.Shares/UpdateVariableModel.cs @@ -0,0 +1,3 @@ +namespace RobotNet.Script.Shares; + +public record UpdateVariableModel(string Name, string Value); diff --git a/RobotNet.Script/ICcLinkIeBasicClient.cs b/RobotNet.Script/ICcLinkIeBasicClient.cs new file mode 100644 index 0000000..adab4d1 --- /dev/null +++ b/RobotNet.Script/ICcLinkIeBasicClient.cs @@ -0,0 +1,415 @@ +namespace RobotNet.Script; + +/// +/// Client CC‑Link IE Basic (SLMP over TCP/UDP) dùng để kết nối PC ↔ PLC Mitsubishi qua Ethernet thường, +/// hỗ trợ đọc/ghi device, polling chu kỳ và gửi lệnh SLMP thô. +/// +public interface ICcLinkIeBasicClient : IAsyncDisposable, IDisposable +{ + /// + /// Trạng thái đã kết nối phiên transport (TCP/UDP) tới PLC hay chưa. + /// + bool IsConnected { get; } + + /// + /// Số lần thử gửi lại khi lỗi tạm thời/timeout. + /// + int RetryCount { get; set; } + + /// + /// Ngắt kết nối transport và giải phóng tài nguyên liên quan. + /// + /// Token hủy bất đồng bộ. + Task DisconnectAsync(CancellationToken ct = default); + + /// + /// Đọc mảng bit (X/Y/M/L/S/B...) từ PLC. + /// + /// Loại device code. + /// Địa chỉ bắt đầu. + /// Số bit cần đọc. + /// Token hủy. + /// Mảng giá trị bit. + Task ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default); + + /// + /// Ghi mảng bit (X/Y/M/L/S/B...) vào PLC. + /// + /// Loại device code. + /// Địa chỉ bắt đầu. + /// Dãy giá trị bit cần ghi. + /// Token hủy. + Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory values, CancellationToken ct = default); + + /// + /// Đọc mảng word (D/W/R/ZR/B...) từ PLC. + /// + /// Loại device code. + /// Địa chỉ bắt đầu (đơn vị word). + /// Số word cần đọc. + /// Token hủy. + /// Mảng word (UInt16). + Task ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default); + + /// + /// Ghi mảng word (D/W/R/ZR/B...) vào PLC. + /// + /// Loại device code. + /// Địa chỉ bắt đầu (đơn vị word). + /// Dãy word cần ghi. + /// Token hủy. + Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory words, CancellationToken ct = default); + + /// + /// Đọc mảng double‑word (UInt32) từ PLC (áp dụng cho vùng hỗ trợ dword). + /// + /// Loại device code. + /// Địa chỉ bắt đầu (đơn vị dword). + /// Số dword cần đọc. + /// Token hủy. + /// Mảng dword (UInt32). + Task ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default); + + /// + /// Ghi mảng double‑word (UInt32) vào PLC (áp dụng cho vùng hỗ trợ dword). + /// + /// Loại device code. + /// Địa chỉ bắt đầu (đơn vị dword). + /// Dãy dword cần ghi. + /// Token hủy. + Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory dwords, CancellationToken ct = default); + + /// + /// Đọc ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần. + /// + /// Danh sách (device, address) cần đọc. + /// Token hủy. + /// Mảng word theo thứ tự điểm yêu cầu. + Task ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default); + + /// + /// Ghi ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần. + /// + /// Danh sách (device, address) cần ghi. + /// Giá trị word theo thứ tự điểm. + /// Token hủy. + Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory values, CancellationToken ct = default); + + /// + /// Sự kiện bắn ra sau mỗi chu kỳ polling hoàn tất. + /// + event EventHandler? Polled; + + /// + /// Bắt đầu polling chu kỳ một hoặc nhiều vùng device. + /// + /// Tùy chọn polling (chu kỳ, vùng, giới hạn kích thước). + /// Token hủy. + void StartPolling(PollOptions options, CancellationToken ct = default); + + /// + /// Dừng polling chu kỳ. + /// + /// Token hủy. + Task StopPollingAsync(CancellationToken ct = default); + + /// + /// Lấy trạng thái liên kết/SLMP gần nhất (end code, thống kê thời gian phản hồi...). + /// + /// Token hủy. + /// Thông tin trạng thái IE Basic. + Task GetStatusAsync(CancellationToken ct = default); + + /// + /// Kiểm tra liên lạc PLC (ví dụ node monitor) ở mức nhanh/gọn. + /// + /// Token hủy. + /// true nếu phản hồi hợp lệ, ngược lại false. + Task PingAsync(CancellationToken ct = default); + + /// + /// Truy vấn thông tin nhận dạng module/CPU (nếu PLC cho phép). + /// + /// Token hủy. + /// Thông tin định danh CPU/module. + Task IdentifyAsync(CancellationToken ct = default); + + /// + /// Gửi lệnh SLMP thô (mở rộng/đặc thù) và nhận phản hồi. + /// + /// Mã lệnh SLMP. + /// Payload SLMP theo định dạng frame chọn (3E/4E). + /// Token hủy. + /// Phản hồi SLMP gồm end code và payload. + Task SendSlmpAsync(SlmpCommand command, ReadOnlyMemory payload, CancellationToken ct = default); +} + +/// +/// Loại transport cho CC‑Link IE Basic (UDP hoặc TCP). +/// +public enum IeBasicTransport +{ + /// Giao tiếp qua UDP (thường dùng cho IE Basic). + Udp, + + /// Giao tiếp qua TCP. + Tcp +} + +/// +/// Định dạng frame SLMP. +/// +public enum SlmpFrameFormat +{ + /// Khung 3E (Format 3E). + Format3E, + + /// Khung 4E (Format 4E). + Format4E +} + +/// +/// Device code (vùng thiết bị) cấp ứng dụng. +/// +public enum DeviceCode +{ + /// Input (bit). + X, + /// Output (bit). + Y, + /// Internal relay (bit). + M, + /// Latch relay (bit). + L, + /// Link relay (bit/word tùy CPU). + B, + /// Step relay (bit). + S, + /// Data register (word). + D, + /// Link register (word). + W, + /// File register (word). + R, + /// Extended file register (word). + ZR +} + +/// +/// Tham số logic phía local (PC/sender) khi gửi SLMP. +/// +/// Số mạng logic. +/// Địa chỉ I/O module logic (thường 0x03FF). +/// Số đa điểm (nếu dùng). +/// Số trạm logic. +public readonly record struct IeBasicLocal( + byte NetworkNo, + ushort ModuleIoNo, + byte MultidropNo, + byte StationNo +); + +/// +/// Cấu hình đầu xa: địa chỉ IP/Port và tham số logic đích cho SLMP. +/// +/// Địa chỉ IP hoặc hostname PLC/module. +/// Cổng SLMP (ví dụ 5007). +/// Số mạng logic đích. +/// Địa chỉ I/O module logic đích. +/// Số đa điểm (nếu dùng). +/// Số trạm logic đích. +public readonly record struct IeBasicRemote( + string Host, + int Port, + byte NetworkNo, + ushort ModuleIoNo, + byte MultidropNo, + byte StationNo +); + +/// +/// Tùy chọn khởi tạo/kết nối IE Basic client. +/// +public sealed record IeBasicClientOptions( + string Host, + int Port, + byte NetworkNo, + ushort ModuleIoNo, + byte MultidropNo, + byte StationNo +) +{ + /// Transport: UDP hoặc TCP. + public IeBasicTransport Transport { get; init; } = IeBasicTransport.Udp; + + /// Định dạng frame SLMP (3E/4E). + public SlmpFrameFormat FrameFormat { get; init; } = SlmpFrameFormat.Format3E; + + /// Tham số logic phía local. + public IeBasicLocal Local { get; init; } = + new IeBasicLocal(0, 0x03FF, 0, 0); + + /// Timeout cho mỗi yêu cầu. + public TimeSpan Timeout { get; init; } = TimeSpan.FromMilliseconds(1000); + + /// Số lần retry khi lỗi tạm thời. + public int RetryCount { get; init; } = 2; + + /// Tự động reconnect nếu mất liên kết. + public bool AutoReconnect { get; init; } = true; + + /// + /// Quy ước ghép cặp word trong DWord/Float (true: little‑endian word order). + /// + public bool LittleEndianWordOrder { get; init; } = true; +} + +/// +/// Tùy chọn cho polling chu kỳ các vùng device. +/// +public sealed record PollOptions +{ + /// Khoảng thời gian giữa hai lần poll. + public TimeSpan Interval { get; init; } = TimeSpan.FromMilliseconds(20); + + /// Danh sách vùng cần poll (device, địa chỉ bắt đầu, số lượng). + public (DeviceCode Device, int Start, int Count)[] Areas { get; init; } = []; + + /// Giới hạn số word tối đa mỗi chu kỳ (để tránh gói quá lớn). + public int MaxWordsPerCycle { get; init; } = 256; + + /// Gom vùng theo từng loại device để tối ưu số gói. + public bool AlignAreasByDevice { get; init; } = true; +} + +/// +/// Dữ liệu trả về sau mỗi chu kỳ polling. +/// +public sealed class PollUpdatedEventArgs : EventArgs +{ + /// Thời điểm khung dữ liệu được cập nhật. + public DateTimeOffset Timestamp { get; init; } + + /// Mảng bit (nếu có vùng bit được cấu hình). + public ReadOnlyMemory? Bits { get; init; } + + /// Mảng word (nếu có vùng word được cấu hình). + public ReadOnlyMemory? Words { get; init; } + + /// Mảng dword (nếu có vùng dword được cấu hình). + public ReadOnlyMemory? DWords { get; init; } +} + +/// +/// Trạng thái liên kết/SLMP để chẩn đoán và theo dõi. +/// +public sealed record IeBasicStatus +{ + /// Liên kết hiện “đang lên” (có phản hồi) hay không. + public bool LinkUp { get; init; } + + /// Số lỗi liên tiếp gần nhất. + public int ConsecutiveErrors { get; init; } + + /// End code SLMP của lỗi gần nhất (nếu có). + public SlmpEndCode? LastEndCode { get; init; } + + /// Mô tả lỗi gần nhất (nếu có). + public string? LastErrorText { get; init; } + + /// Thời gian phản hồi trung bình ước tính. + public TimeSpan? AvgRtt { get; init; } + + /// Thời gian phản hồi lớn nhất quan sát. + public TimeSpan? MaxRtt { get; init; } +} + +/// +/// Thông tin nhận dạng CPU/module (nếu PLC cho phép truy vấn). +/// +public sealed record ModuleIdentity +{ + /// Dòng CPU (ví dụ iQ‑R, iQ‑F). + public string? CpuSeries { get; init; } + + /// Model cụ thể (ví dụ R04ENCPU). + public string? CpuModel { get; init; } + + /// Phiên bản firmware. + public string? Firmware { get; init; } + + /// Nhà sản xuất (thường là Mitsubishi Electric). + public string? Vendor { get; init; } + + /// Thông tin bổ sung khác (nếu có). + public string? AdditionalInfo { get; init; } +} + +/// +/// Mã lệnh SLMP rút gọn (có thể mở rộng khi hiện thực adapter). +/// +public enum SlmpCommand : ushort +{ + /// Đọc device theo dải liên tục (bit/word). + ReadDevice = 0x0401, + + /// Ghi device theo dải liên tục (bit/word). + WriteDevice = 0x1401, + + /// Đọc ngẫu nhiên nhiều điểm. + ReadRandom = 0x0403, + + /// Ghi ngẫu nhiên nhiều điểm. + WriteRandom = 0x1402, + + /// Node monitor / kiểm tra liên lạc. + NodeMonitor = 0x0619, + + /// Truy vấn thông tin thiết bị/PLC. + DeviceInfo = 0x0601 +} + +/// +/// Mã end code SLMP (hoàn thành/thất bại), gồm cả mã nội bộ ánh xạ lỗi transport/timeout. +/// +public enum SlmpEndCode : ushort +{ + /// Hoàn thành thành công. + Completed = 0x0000, + + /// Lệnh không hợp lệ. + InvalidCommand = 0xC059, + + /// Truy cập bị từ chối. + AccessDenied = 0xC056, + + /// Lỗi phạm vi device. + DeviceRangeError = 0xC051, + + /// Thiết bị bận. + Busy = 0xCEE0, + + /// Quá thời gian chờ (gán nội bộ). + Timeout = 0xFFFF, + + /// Lỗi transport/socket (gán nội bộ). + TransportError = 0xFFFE, + + /// Lỗi không xác định (gán nội bộ). + Unknown = 0xFFFD +} + +/// +/// Phản hồi SLMP thô trả về từ . +/// +public sealed record SlmpResponse +{ + /// End code của phản hồi. + public SlmpEndCode EndCode { get; init; } + + /// Payload nhị phân trả về (theo frame 3E/4E). + public ReadOnlyMemory Payload { get; init; } + + /// Thời điểm nhận gói. + public DateTimeOffset Timestamp { get; init; } +} diff --git a/RobotNet.Script/IConnectionManager.cs b/RobotNet.Script/IConnectionManager.cs new file mode 100644 index 0000000..dc7afdf --- /dev/null +++ b/RobotNet.Script/IConnectionManager.cs @@ -0,0 +1,23 @@ +namespace RobotNet.Script; + +/// +/// Quản lý kết nối với device khác. +/// +public interface IConnectionManager +{ + /// + /// Tạo kết nối Modbus TCP đến một thiết bị với địa chỉ IP, cổng và ID đơn vị cụ thể. + /// + /// + /// + /// + /// + Task ConnectModbusTcp(string ipAddress, int port, byte unitId); + + /// + /// + /// + /// + /// + Task ConnectCcLink(IeBasicClientOptions option); +} diff --git a/RobotNet.Script/ILogger.cs b/RobotNet.Script/ILogger.cs new file mode 100644 index 0000000..9a40136 --- /dev/null +++ b/RobotNet.Script/ILogger.cs @@ -0,0 +1,25 @@ +namespace RobotNet.Script; + +/// +/// Xuất thông tin ghi log cho các thành phần trong RobotNet. +/// +public interface ILogger +{ + /// + /// Xuất thông tin ghi log với mức độ thông tin. + /// + /// + void LogInfo(string message); + + /// + /// Xuất thông tin ghi log với mức độ cảnh báo. + /// + /// + void LogWarning(string message); + + /// + /// Xuất thông tin ghi log với mức độ lỗi. + /// + /// + void LogError(string message); +} diff --git a/RobotNet.Script/IMapManager.cs b/RobotNet.Script/IMapManager.cs new file mode 100644 index 0000000..565cf41 --- /dev/null +++ b/RobotNet.Script/IMapManager.cs @@ -0,0 +1,154 @@ +using RobotNet.Script.Expressions; +using System.Linq.Expressions; + +namespace RobotNet.Script; + +/// +/// Service quản lý các phần tử trong bản đồ. +/// +public interface IMapManager +{ + /// + /// Trả về phần tử theo tên trong bản đồ. + /// + /// + /// + /// + Task GetElement(string map, string name); + + /// + /// Retrieves a node from the specified map by its name. + /// + /// This method performs an asynchronous operation to locate a node within the specified map. + /// Ensure that the map and name parameters are valid and non-empty before calling this method. + /// The identifier of the map from which to retrieve the node. Cannot be null or empty. + /// The name of the node to retrieve. Cannot be null or empty. + /// A task that represents the asynchronous operation. The task result contains the node matching the specified name + /// within the given map, or if no such node exists. + Task GetNode(string map, string name); + + /// + /// Tìm kiếm các phần tử trong bản đồ theo biểu thức. + /// + /// + /// + /// + Task FindElements(string map, string model); + + /// + /// Tìm kiếm các phần tử trong bản đồ theo biểu thức. + /// + /// + /// + /// + /// + Task FindElements(string map, string model, Expression> expr); +} + +/// +/// Thông tin về một nút trong bản đồ. +/// +public interface INode +{ + /// + /// Id của nút. + /// + Guid Id { get; } + + /// + /// Id của map chứa nút này. + /// + Guid MapId { get; } + + /// + /// Tên của nút + /// + string Name { get; } + + /// + /// Tọa độ trục hoành của nút trong không gian 2D. + /// + double X { get; } + + /// + /// Tọa độ trục tung của nút trong không gian 2D. + /// + double Y { get; } + + /// + /// Hướng của nút trong không gian 2D, tính bằng độ. + /// + double Theta { get; } +} + +/// +/// Quản lý thông tin của một phần tử trong bản đồ. +/// +public interface IElement +{ + /// + /// Id của phần tử. + /// + Guid Id { get; } + + /// + /// Id model của phần tử này. + /// + Guid ModelId { get; } + + /// + /// Tên của model mà phần tử này thuộc về. + /// + string ModelName { get; } + + /// + /// Id của nút mà phần tử này thuộc về. + /// + Guid NodeId { get; } + + /// + /// Tên của phần tử. + /// + string Name { get; } + + /// + /// Tạo độ trục hoành tương dối của phần từ so với nút mà nó thuộc về. + /// + double OffsetX { get; } + + /// + /// Tọa độ trục tung tương đối của phần từ so với nút mà nó thuộc về. + /// + double OffsetY { get; } + + /// + /// Tên của nút mà phần tử này thuộc về. + /// + string NodeName { get; } + + /// + /// Tọa độ trục hoành của nút trong không gian 2D. + /// + double X { get; } + + /// + /// Tọa độ trục tung của nút trong không gian 2D. + /// + double Y { get; } + + /// + /// Hướng của phần tử trong không gian 2D, tính bằng độ. + /// + double Theta { get; } + + /// + /// Các thuộc tính của phần tử, bao gồm các thuộc tính bool, double, int và string. + /// + ElementProperties Properties { get; } + + /// + /// Lưu các thay đổi của thuộc tính phần tử vào cơ sở dữ liệu + /// + /// + Task SaveChangesAsync(); +} diff --git a/RobotNet.Script/IModbusTcpClient.cs b/RobotNet.Script/IModbusTcpClient.cs new file mode 100644 index 0000000..1c19254 --- /dev/null +++ b/RobotNet.Script/IModbusTcpClient.cs @@ -0,0 +1,92 @@ +namespace RobotNet.Script; + +/// +/// Quản ký kết nối Modbus TCP tới 1 thiết bị. +/// +public interface IModbusTcpClient : IAsyncDisposable, IDisposable +{ + /// + /// Trạng thái kết nối hiện tại với thiết bị Modbus TCP. + /// + bool IsConnected { get; } + + /// + /// Đọc các đầu vào rời rạc (Discrete Inputs) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + /// + Task ReadDiscreteInputsAsync(UInt16 startingAddress, UInt16 quantity, CancellationToken cancellationToken = default); + + /// + /// Đọc các cuộn dây (Coils) từ thiết bị Modbus TCP. + /// + /// + /// + /// + /// + Task ReadCoilsAsync(UInt16 startingAddress, UInt16 quantity, CancellationToken cancellationToken = default); + + /// + /// Đọc các thanh ghi giữ (Holding Registers) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + /// + Task ReadHoldingRegistersAsync(UInt16 startingAddress, UInt16 quantity, CancellationToken cancellationToken = default); + + /// + /// Đọc các thanh ghi đầu vào (Input Registers) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + /// + Task ReadInputRegistersAsync(UInt16 startingAddress, UInt16 quantity, CancellationToken cancellationToken = default); + + /// + /// Ghi viết một cuộn dây (Coil) đơn lẻ đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + Task WriteSingleCoilAsync(UInt16 startingAddress, bool value, CancellationToken cancellationToken = default); + + /// + /// Ghi viết một thanh ghi đơn lẻ đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + Task WriteSingleRegisterAsync(UInt16 startingAddress, UInt16 value, CancellationToken cancellationToken = default); + + /// + /// Ghi viết nhiều cuộn dây (Coils) đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + Task WriteMultipleCoilsAsync(UInt16 startingAddress, bool[] values, CancellationToken cancellationToken = default); + + /// + /// Ghi viết nhiều thanh ghi (Holding Registers) đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + /// + /// + /// + /// + Task WriteMultipleRegistersAsync(UInt16 startingAddress, UInt16[] values, CancellationToken cancellationToken = default); + + /// + /// Đọc và ghi viết nhiều thanh ghi (Holding Registers) trong một lần gọi với hỗ trợ hủy bỏ. + /// + /// + /// + /// + /// + /// + /// + Task ReadWriteMultipleRegistersAsync(ushort readStart, ushort readCount, ushort writeStart, IReadOnlyList writeValues, CancellationToken cancellationToken = default); +} + diff --git a/RobotNet.Script/IRobot.cs b/RobotNet.Script/IRobot.cs new file mode 100644 index 0000000..7151dac --- /dev/null +++ b/RobotNet.Script/IRobot.cs @@ -0,0 +1,232 @@ +using RobotNet.Script.Expressions; +using System.Linq.Expressions; + +namespace RobotNet.Script; + +/// +/// Service điểu khiển robot trong hệ thống RobotNet. +/// +public interface IRobot +{ + /// + /// Di chuyển robot đến một nút trong bản đồ với action kết thúc. + /// + /// + /// + /// + /// + Task Move(string nodeName, IDictionary> actions, CancellationToken cancellationToken); + + + /// + /// Di chuyển robot đến một nút trong bản đồ với action kết thúc. + /// + /// + /// + /// + /// + /// + Task Move(string nodeName, IDictionary> actions, double lastAngle, CancellationToken cancellationToken); + + /// + /// Di chuyển robot đến một nút trong bản đồ với action kết thúc. + /// + /// + /// + /// + /// + Task Move(string nodeName, IEnumerable lastActions, CancellationToken cancellationToken); + + /// + /// Di chuyển robot đến một nút trong bản đồ với action kết thúc. + /// + /// + /// + /// + /// + /// + Task Move(string nodeName, IEnumerable lastActions, double lastAngle, CancellationToken cancellationToken); + + /// + /// Di chuyển robot đến một nút trong bản đồ mà không cần action kết thúc. + /// + /// + /// + /// + /// + Task Move(string nodeName, double lastAngle, CancellationToken cancellationToken); + + /// + /// Di chuyển robot đến một nút trong bản đồ mà không cần action kết thúc. + /// + /// + /// + /// + Task Move(string nodeName, CancellationToken cancellationToken); + + /// + /// Hủy bỏ hành động di chuyển hiện tại của robot, nếu có. + /// + /// + Task AbortMovement(); + + /// + /// Thực hiện một hành động ngay lập tức trên robot mà không cần chờ đợi. + /// + /// + /// + /// + Task Execute(RobotAction action, CancellationToken cancellationToken); + + + /// + /// Lấy trạng thái của robot + /// + /// + Task GetState(); + + /// + /// Waits for the system to reach a ready state. + /// + /// A token to monitor for cancellation requests. If the token is canceled, the operation will be aborted. + /// A task that represents the asynchronous operation. The task result is if the system + /// reaches a ready state; otherwise, if the operation is canceled or the system fails to + /// become ready. + Task WaitForReady(CancellationToken cancellationToken); + + /// + /// + /// + /// + /// + Task RequestACSIn(string id); + + /// + /// + /// + /// + /// + Task RequestACSOut(string id); + + /// + /// Trả về danh sách các tải trọng hiện tại của robot, bao gồm ID, loại, vị trí và trọng lượng. + /// + /// + Task GetLoads(); + + /// + /// + /// + /// + Task CancelOrder(); + + /// + /// + /// + /// + Task CancelAction(); + + /// + /// Chức năng di chuyển robot giả lập theo tọa độ (x, y) trong không gian 2D. + /// + /// + /// + /// + /// + Task SimMoveStraight(double x, double y, CancellationToken cancellationToken); + + /// + /// Chức năng xoay robot giả lập theo một góc nhất định (đơn vị: độ). + /// + /// + /// + /// + Task SimRotate(double angle, CancellationToken cancellationToken); +} + +/// +/// Service quản lý các robot trong hệ thống RobotNet. +/// +public interface IRobotManager +{ + /// + /// Trả về đối tượng điểu khiển robot theo ID. + /// + /// + /// + Task GetRobotById(string robotId); + + /// + /// Lấy trạng thái của một robot theo ID. + /// + /// + /// + Task GetRobotState(string robotId); + + /// + /// Tìm kiếm các robot trong bản đồ theo tên và mô hình. + /// + /// + /// + /// + Task> SearchRobots(string map, string model); + + /// + /// Tìm kiếm các robot trong bản đồ theo tên và mô hình. + /// + /// + /// + /// + /// + Task> SearchRobots(string map, string model, Expression> expr); +} + +/// +/// Regulates if the action is allowed to be executed during movement and/or parallel to other actions. +/// +public enum BlockingType +{ + /// + /// Action can happen in parallel with others, including movement. + /// + NONE, + + /// + /// Action can happen simultaneously with others, but not while moving. + /// + SOFT, + + /// + /// No other actions can be performed while this action is running. + /// + HARD +} + +/// +/// Mô tả một hành động của robot. +/// +/// +/// +public record RobotAction(string ActionType, BlockingType BlockingType) +{ + /// + /// Danh sách các tham số của hành động robot. + /// + public IDictionary? Parameters { get; set; } +} + +/// +/// Dữ liệu trả về từ các hành động của robot, bao gồm thông tin về thành công hay thất bại và thông điệp mô tả. +/// +/// +/// +public record RobotResult(bool IsSuccess, string Message); + +/// +/// Thông tin về tải trọng của robot, bao gồm ID, loại, vị trí và trọng lượng. +/// +/// +/// +/// +/// +public record RobotLoad(string Id, string Type, string Position, double Weight); diff --git a/RobotNet.Script/IUnixDevice.cs b/RobotNet.Script/IUnixDevice.cs new file mode 100644 index 0000000..9cab62c --- /dev/null +++ b/RobotNet.Script/IUnixDevice.cs @@ -0,0 +1,22 @@ +namespace RobotNet.Script; + +/// +/// Service để đọc và ghi dữ liệu từ các thiết bị Unix. +/// +public interface IUnixDevice +{ + /// + /// Đọc dữ liệu từ thiết bị Unix theo tên và độ dài yêu cầu. + /// + /// + /// + /// + byte[] ReadDev(string name, int length); + + /// + /// Ghi dữ liệu vào thiết bị Unix theo tên. + /// + /// + /// + void WriteDev(string name, byte[] data); +} diff --git a/RobotNet.Script/MissionAttribute.cs b/RobotNet.Script/MissionAttribute.cs new file mode 100644 index 0000000..ac1396c --- /dev/null +++ b/RobotNet.Script/MissionAttribute.cs @@ -0,0 +1,21 @@ +namespace RobotNet.Script; + +/// +/// Trạng thái trả về mỗi step trong quá trình thực hiện nhiệm vụ. +/// +/// +/// +public record MissionState(int Step, string Message); + +/// +/// Khai báo một nhiệm vụ trong hệ thống RobotNet. +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class MissionAttribute(bool isMultipleRun = true) : Attribute +{ + /// + /// Cho phép nhiệm vụ này có thể chạy nhiều lần hay không. + /// + public bool IsMultipleRun { get; } = isMultipleRun; +} diff --git a/RobotNet.Script/RobotNet.Script.csproj b/RobotNet.Script/RobotNet.Script.csproj new file mode 100644 index 0000000..486248c --- /dev/null +++ b/RobotNet.Script/RobotNet.Script.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + True + True + + + + + + + diff --git a/RobotNet.Script/TaskAttribute.cs b/RobotNet.Script/TaskAttribute.cs new file mode 100644 index 0000000..637b3cc --- /dev/null +++ b/RobotNet.Script/TaskAttribute.cs @@ -0,0 +1,20 @@ +namespace RobotNet.Script; + +/// +/// Thuộc tính để đánh dấu một phương thức là một tác vụ định kỳ trong hệ thống RobotNet. +/// +/// +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public class TaskAttribute(int interval, bool autoStart = true) : Attribute +{ + /// + /// Thời gian định kỳ để thực hiện tác vụ, tính bằng giây. + /// + public int Interval { get; } = interval; + + /// + /// Cho phép tác vụ này tự động bắt đầu khi hệ thống khởi động hay không. + /// + public bool AutoStart { get; } = autoStart; +} diff --git a/RobotNet.Script/VariableAttribute.cs b/RobotNet.Script/VariableAttribute.cs new file mode 100644 index 0000000..2904534 --- /dev/null +++ b/RobotNet.Script/VariableAttribute.cs @@ -0,0 +1,14 @@ +namespace RobotNet.Script; + +/// +/// Khai báo biến được phép đọc giá trị từ ngoài hệ thống RobotNet Script +/// +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +public class VariableAttribute(bool publicWrite = false) : Attribute +{ + /// + /// Khai báo biến được phép ghi giá trị từ ngoài hệ thống RobotNet Script + /// + public bool PublicWrite { get; } = publicWrite; +} diff --git a/RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs b/RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs new file mode 100644 index 0000000..04fc089 --- /dev/null +++ b/RobotNet.ScriptManager/Clients/RobotManagerHubClient.cs @@ -0,0 +1,330 @@ +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.Clients; +using RobotNet.RobotShares.Models; +using RobotNet.Script; +using RobotNet.Script.Expressions; +using RobotNet.ScriptManager.Helpers; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Clients; + +public class RobotManagerHubClient(string RobotId, string hubUrl, Func> accessTokenProvider) : HubClient(new Uri(hubUrl), accessTokenProvider), IRobot +{ + private async Task GetRobotStateModel() + { + var result = await Connection.InvokeAsync>(nameof(GetState), RobotId); + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to get robot state: {result.Message}"); + } + else + { + if (result.Data is null) + { + throw new InvalidOperationException("Robot state data is null."); + } + return result.Data; + } + } + + private static string RobotErrorToString(IEnumerable errors) + { + var sb = new System.Text.StringBuilder(); + foreach (var error in errors) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + sb.Append($"{error.ErrorLevel}: {error.ErrorType} {error.ErrorDescription} - {error.ErrorHint}"); + foreach (var reference in error.ErrorReferences ?? []) + { + sb.Append($" [{reference.ReferenceKey}: {reference.ReferenceValue}]"); + } + } + return sb.ToString(); + } + + public async Task GetState() => VDA5050ScriptHelper.ConvertToRobotState(await GetRobotStateModel()); + + public async Task GetLoads() + { + var stateModel = await GetRobotStateModel(); + if (stateModel.Loads is null || stateModel.Loads.Length == 0) + { + return []; + } + return [.. stateModel.Loads.Select(load => new RobotLoad(load.LoadId, load.LoadType, load.LoadPosition, load.Weight))]; + } + + public async Task Execute(RobotAction action, CancellationToken cancellationToken) + { + var state = await GetState(); + if (!state.IsReady) + { + throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện action [{action.ActionType}]!"); + } + + var model = VDA5050ScriptHelper.ConvertToRobotInstantActionModel(RobotId, action); + var result = await Connection.InvokeAsync>("InstantAction", model, cancellationToken); + + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to perform instant action: {result.Message}"); + } + + if (string.IsNullOrEmpty(result.Data)) + { + throw new InvalidOperationException("Instant action ID is empty."); + } + + bool isRunning = true; + bool isSuccess = false; + string message = ""; + while (isRunning) + { + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + message = $"Action [{action.ActionType} is canceled"; + break; + } + + var stateModel = await GetRobotStateModel(); + var stateAction = stateModel.ActionStates.FirstOrDefault(a => a.ActionId == result.Data); + if (stateAction is null) + { + isRunning = false; + message = $"Action [{action.ActionType}] not found in robot state."; + } + else if (stateAction.IsError) + { + isRunning = false; + message = $"Action [{action.ActionType}] failed with result: {stateAction.Action?.ResultDescription}, Error: {RobotErrorToString(stateModel.Errors)}"; + } + else if (stateAction.IsProcessing) + { + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + message = $"Action [{action.ActionType}] wait cancel"; + } + else + { + try + { + await Task.Delay(50, cancellationToken); + } + catch (TaskCanceledException) + { + isRunning = false; + message = $"Action [{action.ActionType}] wait cancel"; + } + } + } + else if (stateAction.IsCompleted) + { + isRunning = false; + isSuccess = true; + } + else + { + isRunning = false; + message = $"Action [{action.ActionType}] is in an unexpected state"; + } + } + return new(isSuccess, message); + } + + public async Task AbortMovement() + { + var result = await Connection.InvokeAsync("CancelOrder", RobotId); + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to cancel robot operation: {result.Message}"); + } + } + + private async Task WaitOrder(string premessage, CancellationToken cancellationToken) + { + bool isRunning = true; + bool isSuccess = false; + string message = ""; + while (isRunning) + { + if (cancellationToken.IsCancellationRequested) + { + message = $"{premessage} is canceled"; + break; + } + var stateModel = await GetRobotStateModel(); + if (stateModel.OrderState.IsError) + { + isRunning = false; + message = $"{premessage} failed with error: {string.Join("\n\t", stateModel.OrderState.Errors)} \n Robot Error: {RobotErrorToString(stateModel.Errors)}"; + } + else if (stateModel.OrderState.IsProcessing) + { + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + message = $"{premessage} is canceled"; + } + else + { + try + { + await Task.Delay(50, cancellationToken); + } + catch (TaskCanceledException) + { + isRunning = false; + message = $"{premessage} is canceled"; + } + } + } + else if (stateModel.OrderState.IsCompleted) + { + isRunning = false; + isSuccess = true; + } + else + { + isRunning = false; + message = $"{premessage} is in an unexpected state"; + } + } + return new(isSuccess, message); + } + + private async Task MoveToNode(string nodeName, IDictionary> actions, double? lastAngle, CancellationToken cancellationToken) + { + var state = await GetState(); + if (!state.IsReady) + { + throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện di chuyển đến [{nodeName}]!"); + } + + var model = VDA5050ScriptHelper.ConvertToRobotMoveToNodeModel(RobotId, nodeName, actions, lastAngle); + + var result = await Connection.InvokeAsync("MoveToNode", model, cancellationToken); + + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to move to node: {result.Message}"); + } + + return await WaitOrder($"Robot move to node [{nodeName}]", cancellationToken); + } + + public Task Move(string nodeName, IDictionary> actions, double lastAngle, CancellationToken cancellationToken) + => MoveToNode(nodeName, actions, lastAngle, cancellationToken); + + public Task Move(string nodeName, IDictionary> actions, CancellationToken cancellationToken) + => MoveToNode(nodeName, actions, null, cancellationToken); + + public Task Move(string nodeName, IEnumerable lastActions, double lastAngle, CancellationToken cancellationToken) + => MoveToNode(nodeName, new Dictionary> { { nodeName, lastActions } }, lastAngle, cancellationToken); + + public Task Move(string nodeName, IEnumerable lastActions, CancellationToken cancellationToken) + => MoveToNode(nodeName, new Dictionary> { { nodeName, lastActions } }, null, cancellationToken); + + public Task Move(string nodeName, double lastAngle, CancellationToken cancellationToken) + => MoveToNode(nodeName, new Dictionary>(), lastAngle, cancellationToken); + + public Task Move(string nodeName, CancellationToken cancellationToken) + => MoveToNode(nodeName, new Dictionary>(), null, cancellationToken); + + public async Task SimMoveStraight(double x, double y, CancellationToken cancellationToken) + { + var state = await GetState(); + if (!state.IsReady) + { + throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện di chuyển đến [{x}; {y}]!"); + } + + var result = await Connection.InvokeAsync("MoveStraight", new RobotMoveStraightModel() + { + RobotId = RobotId, + X = x, + Y = y + }, cancellationToken); + + + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to move straight to [{x}; {y}]: {result.Message}"); + } + + return await WaitOrder($"Robot move straight to [{x}; {y}]", cancellationToken); + } + + public async Task SimRotate(double angle, CancellationToken cancellationToken) + { + var state = await GetState(); + if (!state.IsReady) + { + throw new InvalidOperationException($"Robot '{RobotId}' không sẵn sàng để thực hiện rotate to [{angle}]!"); + } + + var result = await Connection.InvokeAsync("Rotate", new RobotRotateModel() + { + RobotId = RobotId, + Angle = angle + }, cancellationToken); + + + if (!result.IsSuccess) + { + throw new InvalidOperationException($"Failed to rotate to [{angle}]: {result.Message}"); + } + + return await WaitOrder($"Robot rotate to [{angle}]", cancellationToken); + } + + public async Task WaitForReady(CancellationToken cancellationToken) + { + var state = await GetState(); + while(!state.IsReady && !cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(50, cancellationToken); + } + catch (TaskCanceledException) + { + return false; // Operation was canceled + } + state = await GetState(); + } + if (cancellationToken.IsCancellationRequested) + { + return false; // Operation was canceled + } + return state.IsReady; + } + + public async Task RequestACSIn(string id) + { + var result = await Connection.InvokeAsync(nameof(RequestACSIn), RobotId, id); + return result.IsSuccess; + } + + public async Task RequestACSOut(string id) + { + var result = await Connection.InvokeAsync(nameof(RequestACSOut), RobotId, id); + return result.IsSuccess; + } + + public async Task CancelOrder() + { + var result = await Connection.InvokeAsync(nameof(CancelOrder), RobotId); + return result.IsSuccess; + } + + public async Task CancelAction() + { + var result = await Connection.InvokeAsync(nameof(CancelOrder), RobotId); + return result.IsSuccess; + } +} diff --git a/RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs b/RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs new file mode 100644 index 0000000..e50ce10 --- /dev/null +++ b/RobotNet.ScriptManager/Connections/CcLinkIeBasicClient.cs @@ -0,0 +1,679 @@ +using RobotNet.Script; +using System.Buffers.Binary; +using System.Net; +using System.Net.Sockets; + +namespace RobotNet.ScriptManager.Connections; + +public class CcLinkIeBasicClient(IeBasicClientOptions options) : ICcLinkIeBasicClient +{ + private readonly SemaphoreSlim _sendLock = new(1, 1); + private UdpClient? _udp; + private TcpClient? _tcp; + private NetworkStream? _tcpStream; + private IPEndPoint? _remoteEp; + private CancellationTokenSource? _pollCts; + + private IeBasicStatus _status = new() + { + LinkUp = false, + ConsecutiveErrors = 0, + LastEndCode = null, + LastErrorText = null, + AvgRtt = null, + MaxRtt = null + }; + + private volatile bool _connected; + private long _rttCount; + private long _rttSumTicks; + + public bool IsConnected => _connected; + public int RetryCount { get => options.RetryCount; set => options = options with { RetryCount = value }; } + + public event EventHandler? Polled; + + // ====== Public API ====== + + public async Task ConnectAsync(CancellationToken ct = default) + { + if (options.FrameFormat != SlmpFrameFormat.Format3E) + throw new NotSupportedException("Triển khai tham chiếu này hiện hỗ trợ 3E binary frame."); + + if (options.Transport == IeBasicTransport.Udp) + { + _udp?.Dispose(); + _udp = new UdpClient(); + _udp.Client.ReceiveTimeout = (int)options.Timeout.TotalMilliseconds; + _udp.Client.SendTimeout = (int)options.Timeout.TotalMilliseconds; + + var ip = await ResolveHostAsync(options.Host, ct); + _remoteEp = new IPEndPoint(ip, options.Port); + _udp.Connect(_remoteEp); + } + else + { + _tcp?.Close(); + _tcp = new TcpClient + { + NoDelay = true, + ReceiveTimeout = (int)options.Timeout.TotalMilliseconds, + SendTimeout = (int)options.Timeout.TotalMilliseconds + }; + + var ip = await ResolveHostAsync(options.Host, ct); + await _tcp.ConnectAsync(ip, options.Port, ct); + _tcpStream = _tcp.GetStream(); + } + + _connected = true; + _status = _status with { LinkUp = true, ConsecutiveErrors = 0, LastErrorText = null, LastEndCode = null }; + } + + public Task DisconnectAsync(CancellationToken ct = default) + { + StopPollingInternal(); + + _connected = false; + + try { _tcpStream?.Dispose(); } catch { } + try { _tcp?.Close(); } catch { } + try { _udp?.Dispose(); } catch { } + + _tcpStream = null; + _tcp = null; + _udp = null; + + _status = _status with { LinkUp = false }; + return Task.CompletedTask; + } + + public async Task ReadBitsAsync(DeviceCode device, int startAddress, int count, CancellationToken ct = default) + { + // 3E binary: Command 0x0401, Subcommand 0x0001 (bit units) + var payload = BuildBatchRead(device, startAddress, count, isBit: true); + var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct); + EnsureOk(resp); + + // Bit data: packed per bit (LSB-first) theo manual. Ở đây đơn giản dùng từng byte -> từng bit. + return UnpackBits(resp.Payload.Span, count); + } + + public async Task WriteBitsAsync(DeviceCode device, int startAddress, ReadOnlyMemory values, CancellationToken ct = default) + { + // 3E binary: Command 0x1401, Subcommand 0x0001 (bit units) + var payload = BuildBatchWriteBits(device, startAddress, values.Span); + var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct); + EnsureOk(resp); + } + + public async Task ReadWordsAsync(DeviceCode device, int startAddress, int wordCount, CancellationToken ct = default) + { + // 3E binary: Command 0x0401, Subcommand 0x0000 (word units) + var payload = BuildBatchRead(device, startAddress, wordCount, isBit: false); + var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct); + EnsureOk(resp); + + // Word data: little-endian UInt16 + if (resp.Payload.Length < wordCount * 2) + throw new InvalidOperationException("Payload ngắn hơn mong đợi."); + var span = resp.Payload.Span; + var result = new ushort[wordCount]; + for (int i = 0; i < wordCount; i++) + result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2)); + return result; + } + + public async Task WriteWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory words, CancellationToken ct = default) + { + // 3E binary: Command 0x1401, Subcommand 0x0000 (word units) + var payload = BuildBatchWriteWords(device, startAddress, words.Span); + var resp = await SendSlmpAsync(SlmpCommand.WriteDevice, payload, ct); + EnsureOk(resp); + } + + public async Task ReadDWordsAsync(DeviceCode device, int startAddress, int dwordCount, CancellationToken ct = default) + { + if (SupportsDirectDWordRead(device)) + { + // 3E binary: Command 0x0401, Subcommand 0x0002 (dword units) + var payload = BuildBatchRead(device, startAddress, dwordCount, isBit: false, isDWord: true); + var resp = await SendSlmpAsync(SlmpCommand.ReadDevice, payload, ct); + EnsureOk(resp); + + if (resp.Payload.Length < dwordCount * 4) + throw new InvalidOperationException("Payload ngắn hơn mong đợi."); + var span = resp.Payload.Span; + var result = new uint[dwordCount]; + for (int i = 0; i < dwordCount; i++) + result[i] = BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(i * 4, 4)); + return result; + } + else + { + // Đọc dword như đọc 2 word/liên tiếp + var words = await ReadWordsAsync(device, startAddress, dwordCount * 2, ct); + var result = new uint[dwordCount]; + + if (options.LittleEndianWordOrder) + { + for (int i = 0, j = 0; i < dwordCount; i++, j += 2) + result[i] = (uint)(words[j] | ((uint)words[j + 1] << 16)); + } + else + { + for (int i = 0, j = 0; i < dwordCount; i++, j += 2) + result[i] = (uint)(words[j + 1] | ((uint)words[j] << 16)); + } + return result; + } + } + + public async Task WriteDWordsAsync(DeviceCode device, int startAddress, ReadOnlyMemory dwords, CancellationToken ct = default) + { + var span = dwords.Span; + var buf = new ushort[span.Length * 2]; + + if (options.LittleEndianWordOrder) + { + for (int i = 0, j = 0; i < span.Length; i++, j += 2) + { + buf[j] = (ushort)(span[i] & 0xFFFF); + buf[j + 1] = (ushort)((span[i] >> 16) & 0xFFFF); + } + } + else + { + for (int i = 0, j = 0; i < span.Length; i++, j += 2) + { + buf[j] = (ushort)((span[i] >> 16) & 0xFFFF); + buf[j + 1] = (ushort)(span[i] & 0xFFFF); + } + } + await WriteWordsAsync(device, startAddress, buf, ct); + } + + public async Task ReadRandomWordsAsync((DeviceCode Device, int Address)[] points, CancellationToken ct = default) + { + // 3E binary: Read Random (0x0403). Payload: điểm (addr+devcode) + var payload = BuildRandomRead(points); + var resp = await SendSlmpAsync(SlmpCommand.ReadRandom, payload, ct); + EnsureOk(resp); + + // Trả về word liên tiếp + var span = resp.Payload.Span; + if (span.Length < points.Length * 2) throw new InvalidOperationException("Payload ngắn hơn mong đợi."); + var result = new ushort[points.Length]; + for (int i = 0; i < points.Length; i++) + result[i] = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i * 2, 2)); + return result; + } + + public async Task WriteRandomWordsAsync((DeviceCode Device, int Address)[] points, ReadOnlyMemory values, CancellationToken ct = default) + { + if (points.Length != values.Length) + throw new ArgumentException("Số điểm và số giá trị không khớp."); + + var payload = BuildRandomWrite(points, values.Span); + var resp = await SendSlmpAsync(SlmpCommand.WriteRandom, payload, ct); + EnsureOk(resp); + } + + public async Task GetStatusAsync(CancellationToken ct = default) + { + // Có thể gửi NodeMonitor để “làm nóng” trạng thái; ở đây trả về snapshot hiện có + await Task.Yield(); + return _status; + } + + public async Task PingAsync(CancellationToken ct = default) + { + try + { + // Node monitor: dùng CMD DeviceInfo/NodeMonitor rỗng để kiểm phản hồi + var resp = await SendSlmpAsync(SlmpCommand.NodeMonitor, ReadOnlyMemory.Empty, ct); + return resp.EndCode == SlmpEndCode.Completed; + } + catch + { + return false; + } + } + + public async Task IdentifyAsync(CancellationToken ct = default) + { + // Tối giản: gửi DeviceInfo và parse sơ bộ (tuỳ PLC mà payload khác nhau) + var resp = await SendSlmpAsync(SlmpCommand.DeviceInfo, ReadOnlyMemory.Empty, ct); + // Ở đây không chuẩn hoá do payload phụ thuộc model; trả về khung rỗng để người dùng tự giải. + return new ModuleIdentity + { + Vendor = "Mitsubishi Electric", + AdditionalInfo = $"Raw bytes: {resp.Payload.Length} (xem manual model để parse)" + }; + } + + public async Task SendSlmpAsync(SlmpCommand command, ReadOnlyMemory payload, CancellationToken ct = default) + { + if (!_connected) throw new InvalidOperationException("Chưa kết nối."); + + // Dựng 3E frame + var req = Build3EFrame(options, (ushort)command, subcommand: CurrentSubcommandHint, payload); + + // Gửi/nhận với retry + var sw = System.Diagnostics.Stopwatch.StartNew(); + for (int attempt = 0; attempt <= RetryCount; attempt++) + { + try + { + var rawResp = await SendAndReceiveAsync(req, ct); + var resp = Parse3EResponse(rawResp); + + sw.Stop(); + TrackRtt(sw.Elapsed); + + _status = _status with + { + LinkUp = true, + ConsecutiveErrors = 0, + LastEndCode = resp.EndCode, + LastErrorText = null, + AvgRtt = TimeSpan.FromTicks((_rttCount > 0) ? _rttSumTicks / _rttCount : 0), + MaxRtt = (_status.MaxRtt == null || sw.Elapsed > _status.MaxRtt) ? sw.Elapsed : _status.MaxRtt + }; + return resp; + } + catch (Exception ex) when (attempt < RetryCount) + { + _status = _status with + { + LinkUp = false, + ConsecutiveErrors = _status.ConsecutiveErrors + 1, + LastErrorText = ex.Message, + LastEndCode = SlmpEndCode.TransportError + }; + // thử lại + await Task.Delay(10, ct); + } + } + + sw.Stop(); + throw new TimeoutException("SLMP: Hết retry mà vẫn lỗi."); + } + + public async ValueTask DisposeAsync() + { + await DisconnectAsync(); + _sendLock.Dispose(); + GC.SuppressFinalize(this); + } + + public void Dispose() + { + DisposeAsync().AsTask().GetAwaiter().GetResult(); + GC.SuppressFinalize(this); + } + + public void StartPolling(PollOptions options, CancellationToken ct = default) + { + StopPollingInternal(); + _pollCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + var token = _pollCts.Token; + + _ = Task.Run(async () => + { + // Strategy: gom vùng bit và word/dword riêng để tối ưu + while (!token.IsCancellationRequested && _connected) + { + try + { + // Đơn giản: chỉ đọc word; nếu có bit thì đọc riêng + var wordsAreas = options.Areas; + ushort[]? words = null; + bool[]? bits = null; + + // (Minh hoạ) Đọc tất cả vùng word liên tiếp bằng cách gộp, bạn có thể tối ưu thêm theo thiết bị. + if (wordsAreas.Length > 0) + { + // Đây đọc lần lượt từng vùng cho đơn giản (dễ hiểu; bạn có thể hợp nhất để giảm gói) + var aggWords = new System.Collections.Generic.List(); + foreach (var (Device, Start, Count) in wordsAreas) + { + if (Device == DeviceCode.X || Device == DeviceCode.Y || Device == DeviceCode.M || + Device == DeviceCode.L || Device == DeviceCode.S || Device == DeviceCode.B) + { + // Bit area + var b = await ReadBitsAsync(Device, Start, Count, token); + // Nối bit (minh hoạ): không ghép; đẩy riêng + bits = b; + } + else + { + var w = await ReadWordsAsync(Device, Start, Count, token); + aggWords.AddRange(w); + } + } + if (aggWords.Count > 0) words = [.. aggWords]; + } + + Polled?.Invoke(this, new PollUpdatedEventArgs + { + Timestamp = DateTimeOffset.UtcNow, + Bits = bits, + Words = words, + DWords = null + }); + + await Task.Delay(options.Interval, token); + } + catch (OperationCanceledException) { } + catch + { + // bỏ qua vòng lỗi và tiếp tục (tuỳ nhu cầu: bạn có thể tăng backoff) + await Task.Delay(TimeSpan.FromMilliseconds(50), token); + } + } + }, token); + } + + public Task StopPollingAsync(CancellationToken ct = default) + { + StopPollingInternal(); + return Task.CompletedTask; + } + + // ====== Internal helpers ====== + + private void StopPollingInternal() + { + try { _pollCts?.Cancel(); } catch { } + try { _pollCts?.Dispose(); } catch { } + _pollCts = null; + } + + private static async Task ResolveHostAsync(string host, CancellationToken ct) + { + if (IPAddress.TryParse(host, out var ip)) return ip; + var entry = await Dns.GetHostEntryAsync(host, ct); + foreach (var addr in entry.AddressList) + if (addr.AddressFamily == AddressFamily.InterNetwork) return addr; + throw new InvalidOperationException("Không tìm thấy IPv4 cho host."); + } + + private void TrackRtt(TimeSpan elapsed) + { + var ticks = elapsed.Ticks; + System.Threading.Interlocked.Add(ref _rttSumTicks, ticks); + System.Threading.Interlocked.Increment(ref _rttCount); + } + + private static void EnsureOk(SlmpResponse resp) + { + if (resp.EndCode != SlmpEndCode.Completed) + throw new InvalidOperationException($"SLMP EndCode: 0x{(ushort)resp.EndCode:X4}"); + } + + // ====== SLMP 3E Binary Build/Parse ====== + + // Gợi ý subcommand hiện hành (đặt trước khi Build3EFrame): 0x0000=word, 0x0001=bit + private ushort CurrentSubcommandHint = 0x0000; + + private static byte[] Build3EFrame(IeBasicClientOptions opt, ushort command, ushort subcommand, ReadOnlyMemory userData) + { + // Tài liệu 3E binary (TCP): Subheader 0x5000; (UDP): 0x5400 + // Header: + // [0-1] Subheader (LE) + // [2] Network No + // [3] PC No (để 0x00) + // [4-5] Module I/O No (LE) + // [6] Multidrop No + // [7-8] Data Length (LE) = 2(timer) + 2(cmd) + 2(subcmd) + payload.Length + // [9-10] Timer (LE) đơn vị 250 ms hoặc 1? (tuỳ manual; thường 0x0010 ~ 4s). Ở đây lấy theo Timeout ~ ms/10. + // [11-12] Command (LE) + // [13-14] Subcommand (LE) + // [15..] Payload + + var subheader = (opt.Transport == IeBasicTransport.Udp) ? (ushort)0x5400 : (ushort)0x5000; + var dataLen = (ushort)(2 + 2 + 2 + userData.Length); // timer + cmd + subcmd + payload + + // Timer: convert từ Timeout ~ ms -> giá trị phù hợp. Ở đây minh hoạ: ms/10 (tuỳ manual). + var timer = (ushort)Math.Clamp((int)(opt.Timeout.TotalMilliseconds / 10.0), 1, 0xFFFE); + + var buf = new byte[15 + userData.Length]; + + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), subheader); + buf[2] = opt.NetworkNo; + buf[3] = 0x00; // PC No + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), opt.ModuleIoNo); + buf[6] = opt.MultidropNo; + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(7, 2), dataLen); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(9, 2), timer); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(11, 2), command); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(13, 2), subcommand); + userData.Span.CopyTo(buf.AsSpan(15)); + + return buf; + } + + private async Task SendAndReceiveAsync(byte[] request, CancellationToken ct) + { + await _sendLock.WaitAsync(ct).ConfigureAwait(false); + + try + { + if (options.Transport == IeBasicTransport.Udp) + { + if (_udp is null) throw new InvalidOperationException("UDP client null."); + await _udp.SendAsync(request, request.Length); + var recv = await _udp.ReceiveAsync(ct); + return recv.Buffer; + } + else + { + if (_tcpStream is null) throw new InvalidOperationException("TCP stream null."); + + // Gửi request + await _tcpStream.WriteAsync(request, ct); + await _tcpStream.FlushAsync(ct); + + // Đọc header tối thiểu 15 bytes để biết data length + var header = new byte[15]; + await ReadExactAsync(_tcpStream, header, ct); + + // Lấy data length + var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(header.AsSpan(7, 2)); + var rest = new byte[dataLen]; // bao gồm timer/cmd/subcmd/endcode/payload (ở response thay timer bằng EndCode) + await ReadExactAsync(_tcpStream, rest, ct); + + // Gộp lại thành frame đầy đủ cho parser + var combined = new byte[header.Length + rest.Length]; + Buffer.BlockCopy(header, 0, combined, 0, header.Length); + Buffer.BlockCopy(rest, 0, combined, header.Length, rest.Length); + return combined; + } + } + finally + { + _sendLock.Release(); + } + } + + private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken ct) + { + int read = 0; + while (read < buffer.Length) + { + var n = await stream.ReadAsync(buffer.AsMemory(read, buffer.Length - read), ct); + if (n == 0) throw new SocketException((int)SocketError.ConnectionReset); + read += n; + } + } + + private static SlmpResponse Parse3EResponse(byte[] frame) + { + // Response 3E: + // [0-1] Subheader + // [2] NetworkNo + // [3] PCNo + // [4-5] ModuleIoNo + // [6] MultidropNo + // [7-8] Data Length (LE) + // [9-10] End Code (LE) <-- khác request (timer) + // [11..] Payload + if (frame.Length < 11) + throw new InvalidOperationException("Frame quá ngắn."); + + var dataLen = BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(7, 2)); + if (frame.Length < 11 + dataLen) + throw new InvalidOperationException("Frame không đủ chiều dài."); + + var endCode = (SlmpEndCode)BinaryPrimitives.ReadUInt16LittleEndian(frame.AsSpan(9, 2)); + var payload = new ReadOnlyMemory(frame, 11, dataLen - 2); // trừ endcode (2B) + return new SlmpResponse { EndCode = endCode, Payload = payload, Timestamp = DateTimeOffset.UtcNow }; + } + + // ====== Payload builders (3E binary) ====== + + private static byte DevCodeBinary(DeviceCode device) => device switch + { + // Mã phổ biến trong MC 3E binary (tham chiếu; kiểm tra manual cho chắc theo CPU): + DeviceCode.X => 0x9C, + DeviceCode.Y => 0x9D, + DeviceCode.M => 0x90, + DeviceCode.L => 0x92, + DeviceCode.B => 0xA0, + DeviceCode.S => 0x98, + DeviceCode.D => 0xA8, + DeviceCode.W => 0xB4, + DeviceCode.R => 0xAF, + DeviceCode.ZR => 0xB0, + _ => 0x00 + }; + + private ReadOnlyMemory BuildBatchWriteWords(DeviceCode device, int start, ReadOnlySpan words) + { + CurrentSubcommandHint = 0x0000; // word units + + var buf = new byte[3 + 1 + 2 + words.Length * 2]; + buf[0] = (byte)(start & 0xFF); + buf[1] = (byte)((start >> 8) & 0xFF); + buf[2] = (byte)((start >> 16) & 0xFF); + buf[3] = DevCodeBinary(device); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)words.Length); + + var off = 6; + for (int i = 0; i < words.Length; i++, off += 2) + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off, 2), words[i]); + + return buf; + } + + private ReadOnlyMemory BuildBatchWriteBits(DeviceCode device, int start, ReadOnlySpan bits) + { + CurrentSubcommandHint = 0x0001; // bit units + + // Bit packing theo MC (1 bit/byte hoặc 2 bit/byte tuỳ subcmd). Ở đây dùng 1 bit/bit (1 byte/bit) để đơn giản; + // nhiều PLC chấp nhận. Nếu cần tối ưu, gói bit theo nibble. + var buf = new byte[3 + 1 + 2 + bits.Length]; + buf[0] = (byte)(start & 0xFF); + buf[1] = (byte)((start >> 8) & 0xFF); + buf[2] = (byte)((start >> 16) & 0xFF); + buf[3] = DevCodeBinary(device); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)bits.Length); + + for (int i = 0; i < bits.Length; i++) + buf[6 + i] = (byte)(bits[i] ? 0x01 : 0x00); + + return buf; + } + + private static ReadOnlyMemory BuildRandomRead((DeviceCode Device, int Address)[] points) + { + // Format tối giản: [count(2)] + N * (addr(3) + dev(1)) + var buf = new byte[2 + points.Length * 4]; + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length); + var off = 2; + foreach (var (Device, Address) in points) + { + buf[off + 0] = (byte)(Address & 0xFF); + buf[off + 1] = (byte)((Address >> 8) & 0xFF); + buf[off + 2] = (byte)((Address >> 16) & 0xFF); + buf[off + 3] = DevCodeBinary(Device); + off += 4; + } + return buf; + } + + private static ReadOnlyMemory BuildRandomWrite((DeviceCode Device, int Address)[] points, ReadOnlySpan values) + { + // [count(2)] + N*(addr(3)+dev(1)+value(2)) + var buf = new byte[2 + points.Length * (4 + 2)]; + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(0, 2), (ushort)points.Length); + var off = 2; + for (int i = 0; i < points.Length; i++) + { + var (Device, Address) = points[i]; + buf[off + 0] = (byte)(Address & 0xFF); + buf[off + 1] = (byte)((Address >> 8) & 0xFF); + buf[off + 2] = (byte)((Address >> 16) & 0xFF); + buf[off + 3] = DevCodeBinary(Device); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(off + 4, 2), values[i]); + off += 6; + } + return buf; + } + + private static bool[] UnpackBits(ReadOnlySpan data, int expectedCount) + { + // Đơn giản hoá: nếu payload trả về mỗi bit = 1 byte (0x00/0x01) + // Nếu PLC trả về packed bits, bạn cần giải theo bit-level. Ở đây xử lý cả hai: + if (data.Length == expectedCount) + { + var res = new bool[expectedCount]; + for (int i = 0; i < expectedCount; i++) res[i] = data[i] != 0; + return res; + } + else + { + // Giải packed: LSB-first trong từng byte + var res = new bool[expectedCount]; + int idx = 0; + for (int b = 0; b < data.Length && idx < expectedCount; b++) + { + byte val = data[b]; + for (int bit = 0; bit < 8 && idx < expectedCount; bit++, idx++) + res[idx] = ((val >> bit) & 0x01) != 0; + } + return res; + } + } + + private ReadOnlyMemory BuildBatchRead(DeviceCode device, int start, int count, bool isBit, bool isDWord = false) + { + // Request data: + // [0-2] Address (3 bytes, little-endian 24-bit) + // [3] Device Code (1 byte) + // [4-5] Points/Count (LE, word/bit/dword tuỳ subcmd) + // [6-7] Reserved? (tuỳ lệnh) - KHÔNG cần cho 0x0401 + // Ở đây dùng định dạng tối giản: addr(3) + dev(1) + count(2) + subcmd đã set ở header + + if (isDWord) + CurrentSubcommandHint = 0x0002; // dword units + else + CurrentSubcommandHint = isBit ? (ushort)0x0001 : (ushort)0x0000; + + var buf = new byte[3 + 1 + 2]; + // 24-bit little-endian + buf[0] = (byte)(start & 0xFF); + buf[1] = (byte)((start >> 8) & 0xFF); + buf[2] = (byte)((start >> 16) & 0xFF); + buf[3] = DevCodeBinary(device); + BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(4, 2), (ushort)count); + return buf; + } + + // 5. Hàm kiểm tra device có hỗ trợ đọc dword trực tiếp không (tuỳ CPU, ví dụ D, W, R, ZR thường hỗ trợ) + private static bool SupportsDirectDWordRead(DeviceCode device) + { + return device == DeviceCode.D || device == DeviceCode.W || device == DeviceCode.R || device == DeviceCode.ZR; + } +} diff --git a/RobotNet.ScriptManager/Connections/ModbusTcpClient.cs b/RobotNet.ScriptManager/Connections/ModbusTcpClient.cs new file mode 100644 index 0000000..af138ee --- /dev/null +++ b/RobotNet.ScriptManager/Connections/ModbusTcpClient.cs @@ -0,0 +1,488 @@ +using RobotNet.Script; +using System.Buffers.Binary; +using System.Net.Sockets; + +namespace RobotNet.ScriptManager.Connections; + +public sealed class ModbusTcpClient(string host, int port = 502, byte unitId = 1) : IModbusTcpClient +{ + public string Host { get; } = host; + public int Port { get; } = port; + public byte UnitId { get; } = unitId; + public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(2); + public TimeSpan ConnectTimeout { get; set; } = TimeSpan.FromSeconds(3); + public int MaxRetries { get; set; } = 2; // per request + public bool AutoReconnect { get; set; } = true; + public bool KeepAlive { get; set; } = true; // enable TCP keepalive + public TimeSpan HeartbeatInterval { get; set; } = TimeSpan.FromSeconds(10); + public ushort HeartbeatRegisterAddress { get; set; } = 0; // dummy read address for heartbeat + + private TcpClient? _client; + private NetworkStream? _stream; + private readonly SemaphoreSlim _sendLock = new(1, 1); + private int _transactionId = 0; + private CancellationTokenSource? _cts; + private Task? _heartbeatTask; + + #region Connect/Dispose + public async Task ConnectAsync(CancellationToken ct = default) + { + await CloseAsync().ConfigureAwait(false); + _cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + var cts = _cts; + + _client = new TcpClient + { + NoDelay = true, + LingerState = new LingerOption(true, 0) + }; + + using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(ct); + connectCts.CancelAfter(ConnectTimeout); + var connectTask = _client.ConnectAsync(Host, Port); + using (connectCts.Token.Register(() => SafeCancel(connectTask))) + { + await connectTask.ConfigureAwait(false); + } + + if (KeepAlive) + { + try + { + SetKeepAlive(_client.Client, true, keepAliveTimeMs: 15_000, keepAliveIntervalMs: 5_000); + } + catch { /* best-effort */ } + } + + _stream = _client.GetStream(); + _stream.ReadTimeout = (int)RequestTimeout.TotalMilliseconds; + _stream.WriteTimeout = (int)RequestTimeout.TotalMilliseconds; + + if (HeartbeatInterval > TimeSpan.Zero) + { + _heartbeatTask = Task.Run(() => HeartbeatLoopAsync(cts!.Token), CancellationToken.None); + } + } + + public bool IsConnected => _client?.Connected == true && _stream != null; + + public async ValueTask DisposeAsync() + { + await CloseAsync().ConfigureAwait(false); + _sendLock.Dispose(); + GC.SuppressFinalize(this); + } + + public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); + + private async Task CloseAsync() + { + try { _cts?.Cancel(); } catch { } + try { if (_heartbeatTask != null) await Task.WhenAny(_heartbeatTask, Task.Delay(50)); } catch { } + try { _stream?.Dispose(); } catch { } + try { _client?.Close(); } catch { } + _stream = null; + _client = null; + _cts?.Dispose(); + _cts = null; + _heartbeatTask = null; + } + #endregion + + #region Public Modbus API + // Coils & Discretes + public Task ReadCoilsAsync(ushort startAddress, ushort count, CancellationToken ct = default) + => ReadBitsAsync(0x01, startAddress, count, ct); + + public Task ReadDiscreteInputsAsync(ushort startAddress, ushort count, CancellationToken ct = default) + => ReadBitsAsync(0x02, startAddress, count, ct); + + // Registers + public Task ReadHoldingRegistersAsync(ushort startAddress, ushort count, CancellationToken ct = default) + => ReadRegistersAsync(0x03, startAddress, count, ct); + + public Task ReadInputRegistersAsync(ushort startAddress, ushort count, CancellationToken ct = default) + => ReadRegistersAsync(0x04, startAddress, count, ct); + + public Task WriteSingleCoilAsync(ushort address, bool value, CancellationToken ct = default) + => WriteSingleAsync(0x05, address, value ? (ushort)0xFF00 : (ushort)0x0000, ct); + + public Task WriteSingleRegisterAsync(ushort address, ushort value, CancellationToken ct = default) + => WriteSingleAsync(0x06, address, value, ct); + + public Task WriteMultipleCoilsAsync(ushort startAddress, bool[] values, CancellationToken ct = default) + => WriteMultipleCoilsCoreAsync(startAddress, values, ct); + + public Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] values, CancellationToken ct = default) + => WriteMultipleRegistersCoreAsync(startAddress, values, ct); + + // FC 22: Mask Write Register + public Task MaskWriteRegisterAsync(ushort address, ushort andMask, ushort orMask, CancellationToken ct = default) + => SendExpectEchoAsync(0x16, writer: span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], address); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), andMask); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(4, 2), orMask); + return 6; + }, expectedEchoLength: 6, ct); + + // FC 23: Read/Write Multiple Registers + public async Task ReadWriteMultipleRegistersAsync( + ushort readStart, ushort readCount, + ushort writeStart, IReadOnlyList writeValues, + CancellationToken ct = default) + { + return await SendAsync(0x17, readCount * 2, span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], readStart); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), readCount); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(4, 2), writeStart); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(6, 2), (ushort)writeValues.Count); + span[8] = (byte)(writeValues.Count * 2); + int pos = 9; + for (int i = 0; i < writeValues.Count; i++) + { + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(pos, 2), writeValues[i]); + pos += 2; + } + return pos; // payload length + }, parse: resp => + { + int byteCount = resp[0]; + if (resp.Length != byteCount + 1) throw new ModbusException("Invalid byte count in response"); + var result = new ushort[byteCount / 2]; + for (int i = 0; i < result.Length; i++) + result[i] = BinaryPrimitives.ReadUInt16BigEndian(resp.Slice(1 + i * 2, 2)); + return result; + }, ct).ConfigureAwait(false); + } + + // FC 43/14: Read Device Identification (basic) + public async Task> ReadDeviceIdentificationAsync(byte category = 0x01 /* Basic */ , CancellationToken ct = default) + { + return await SendAsync(0x2B, 0, span => + { + span[0] = 0x0E; // MEI type + span[1] = 0x01; // Read Device ID + span[2] = category; // category + span[3] = 0x00; // object id + return 4; + }, parse: resp => + { + // resp: [MEI, ReadDevId, conformity, moreFollows, nextObjectId, numObjects, objects...] + if (resp.Length < 6) throw new ModbusException("Invalid Device ID response"); + int pos = 0; + byte mei = resp[pos++]; + if (mei != 0x0E) throw new ModbusException("Invalid MEI type"); + pos++; // ReadDevId + pos++; // conformity + pos++; // moreFollows + pos++; // nextObjectId + byte num = resp[pos++]; + var dict = new Dictionary(); + for (int i = 0; i < num; i++) + { + byte id = resp[pos++]; + byte len = resp[pos++]; + if (pos + len > resp.Length) throw new ModbusException("Invalid object length"); + string val = System.Text.Encoding.ASCII.GetString(resp.ToArray(), pos, len); + pos += len; + dict[id] = val; + } + return dict; + }, ct).ConfigureAwait(false); + } + #endregion + + #region Core Send Helpers + private async Task ReadBitsAsync(byte function, ushort startAddress, ushort count, CancellationToken ct) + { + if (count is 0 or > 2000) throw new ArgumentOutOfRangeException(nameof(count)); + var data = await SendAsync(function, expectedLength: (count + 7) / 8 + 1, writer: span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), count); + return 4; + }, parse: resp => resp.ToArray(), ct).ConfigureAwait(false); + + int byteCount = data[0]; + if (byteCount != data.Length - 1) throw new ModbusException("Unexpected byte count"); + var result = new bool[count]; + for (int i = 0; i < count; i++) + { + int b = data[1 + (i / 8)]; + result[i] = ((b >> (i % 8)) & 0x01) == 1; + } + return result; + } + + private async Task ReadRegistersAsync(byte function, ushort startAddress, ushort count, CancellationToken ct) + { + if (count is 0 or > 125) throw new ArgumentOutOfRangeException(nameof(count)); + var data = await SendAsync(function, expectedLength: count * 2 + 1, writer: span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), count); + return 4; + }, parse: resp => resp.ToArray(), ct).ConfigureAwait(false); + + int byteCount = data[0]; + if (byteCount != count * 2 || data.Length != byteCount + 1) + throw new ModbusException("Unexpected byte count"); + var result = new ushort[count]; + for (int i = 0; i < count; i++) + result[i] = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(1 + i * 2, 2)); + return result; + } + + private Task WriteSingleAsync(byte function, ushort address, ushort value, CancellationToken ct) + => SendExpectEchoAsync(function, span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], address); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), value); + return 4; + }, expectedEchoLength: 4, ct); + + private Task WriteMultipleCoilsCoreAsync(ushort startAddress, bool[] values, CancellationToken ct) + { + if (values == null || values.Length == 0 || values.Length > 1968) + throw new ArgumentOutOfRangeException(nameof(values)); + + int byteCount = (values.Length + 7) / 8; + return SendExpectEchoAsync(0x0F, span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), (ushort)values.Length); + span[4] = (byte)byteCount; + int pos = 5; + int bit = 0; + for (int i = 0; i < byteCount; i++) + { + byte b = 0; + for (int j = 0; j < 8 && bit < values.Length; j++, bit++) + if (values[bit]) b |= (byte)(1 << j); + span[pos++] = b; + } + return pos; + }, expectedEchoLength: 4, ct); + } + + private Task WriteMultipleRegistersCoreAsync(ushort startAddress, ushort[] values, CancellationToken ct) + { + if (values == null || values.Length == 0 || values.Length > 123) + throw new ArgumentOutOfRangeException(nameof(values)); + + return SendExpectEchoAsync(0x10, span => + { + BinaryPrimitives.WriteUInt16BigEndian(span[..2], startAddress); + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(2, 2), (ushort)values.Length); + span[4] = (byte)(values.Length * 2); + int pos = 5; + for (int i = 0; i < values.Length; i++) + { + BinaryPrimitives.WriteUInt16BigEndian(span.Slice(pos, 2), values[i]); + pos += 2; + } + return pos; + }, expectedEchoLength: 4, ct); + } + + private async Task SendExpectEchoAsync(byte function, Func, int> writer, int expectedEchoLength, CancellationToken ct) + { + _ = await SendAsync(function, expectedEchoLength + 0, writer: writer, parse: resp => + { + if (resp.Length != expectedEchoLength) throw new ModbusException("Unexpected echo length"); + return 0; + }, ct).ConfigureAwait(false); + } + + private async Task SendAsync(byte function, int expectedLength, Func, int> writer, Func, T> parse, CancellationToken ct) + { + int attempts = 0; + while (true) + { + attempts++; + try + { + await EnsureConnectedAsync(ct).ConfigureAwait(false); + await _sendLock.WaitAsync(ct).ConfigureAwait(false); + try + { + var reqPayload = new byte[260]; // generous for payload + int payloadLen = writer(reqPayload); + var pdu = new byte[payloadLen + 1]; + pdu[0] = function; + Buffer.BlockCopy(reqPayload, 0, pdu, 1, payloadLen); + + var adu = BuildMbap(UnitId, pdu); + await _stream!.WriteAsync(adu, ct).ConfigureAwait(false); + + // Read MBAP (7 bytes), then body + byte[] mbap = await ReadExactAsync(7, ct).ConfigureAwait(false); + ushort transId = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(0, 2)); + ushort proto = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(2, 2)); + ushort len = BinaryPrimitives.ReadUInt16BigEndian(mbap.AsSpan(4, 2)); + byte unit = mbap[6]; + if (proto != 0 || unit != UnitId) throw new ModbusException("Invalid MBAP header"); + if (len < 2) throw new ModbusException("Invalid length"); + + byte[] body = await ReadExactAsync(len - 1, ct).ConfigureAwait(false); // len includes unitId + byte fc = body[0]; + if ((fc & 0x80) != 0) + { + byte ex = body[1]; + throw new ModbusException($"Exception (FC={(function):X2}): {ex}", (ModbusExceptionCode)ex); + } + if (fc != function) throw new ModbusException("Mismatched function in response"); + + var pduData = body.AsSpan(1); + // If caller supplied an expectedLength, do a soft sanity check when applicable + if (expectedLength > 0 && pduData.Length < expectedLength) + { + // some functions have variable lengths; we avoid hard-failing here + } + return parse(pduData); + } + finally + { + _sendLock.Release(); + } + } + catch (Exception ex) when (attempts <= MaxRetries && IsTransient(ex)) + { + if (AutoReconnect) + { + await ReconnectAsync(ct).ConfigureAwait(false); + continue; // retry + } + throw; + } + } + } + + private async Task EnsureConnectedAsync(CancellationToken ct) + { + if (IsConnected) return; + await ConnectAsync(ct).ConfigureAwait(false); + } + + private async Task ReconnectAsync(CancellationToken ct) + { + try { await CloseAsync(); } catch { } + await Task.Delay(100, ct).ConfigureAwait(false); + await ConnectAsync(ct).ConfigureAwait(false); + } + + private byte[] BuildMbap(byte unitId, byte[] pdu) + { + // MBAP: Transaction(2) Protocol(2=0) Length(2) UnitId(1) + // Length = PDU length + 1 (UnitId) + ushort trans = unchecked((ushort)Interlocked.Increment(ref _transactionId)); + var adu = new byte[7 + pdu.Length]; + BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(0, 2), trans); + BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(2, 2), 0); + BinaryPrimitives.WriteUInt16BigEndian(adu.AsSpan(4, 2), (ushort)(pdu.Length + 1)); + adu[6] = unitId; + Buffer.BlockCopy(pdu, 0, adu, 7, pdu.Length); + return adu; + } + + private async Task ReadExactAsync(int length, CancellationToken ct) + { + byte[] buf = new byte[length]; + int read = 0; + while (read < length) + { + int n = await _stream!.ReadAsync(buf.AsMemory(read, length - read), ct).ConfigureAwait(false); + if (n <= 0) throw new IOException("Remote closed the connection"); + read += n; + } + return buf; + } + + private static bool IsTransient(Exception ex) + => ex is SocketException or IOException or TimeoutException; + + private async Task HeartbeatLoopAsync(CancellationToken ct) + { + while (!ct.IsCancellationRequested) + { + try + { + await Task.Delay(HeartbeatInterval, ct).ConfigureAwait(false); + if (ct.IsCancellationRequested) break; + if (IsConnected) + { + // best-effort heartbeat: read one register + using var timeoutCts = new CancellationTokenSource(RequestTimeout); + using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutCts.Token); + await ReadHoldingRegistersAsync(HeartbeatRegisterAddress, 1, linked.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (ct.IsCancellationRequested) + { + break; + } + catch + { + if (AutoReconnect) + { + try { await ReconnectAsync(ct).ConfigureAwait(false); } catch { } + } + } + } + } + + private static void SafeCancel(Task _) + { + try { } catch { } + } + + private static void SetKeepAlive(Socket socket, bool on, int keepAliveTimeMs, int keepAliveIntervalMs) + { + // Windows & Linux support via IOControl (best-effort) + // On Linux, consider sysctl or TCP_KEEP* sockopts when available. + if (!on) return; + byte[] inOptionValues = new byte[12]; + BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(0), 1); + BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(4), (uint)keepAliveTimeMs); + BinaryPrimitives.WriteUInt32LittleEndian(inOptionValues.AsSpan(8), (uint)keepAliveIntervalMs); + const int SIO_KEEPALIVE_VALS = -1744830460; + try { socket.IOControl(SIO_KEEPALIVE_VALS, inOptionValues, null); } catch { } + } + + #endregion +} + +public enum ModbusExceptionCode : byte +{ + IllegalFunction = 0x01, + IllegalDataAddress = 0x02, + IllegalDataValue = 0x03, + SlaveDeviceFailure = 0x04, + Acknowledge = 0x05, + SlaveDeviceBusy = 0x06, + MemoryParityError = 0x08, + GatewayPathUnavailable = 0x0A, + GatewayTargetFailedToRespond = 0x0B +} + +public sealed class ModbusException(string message, ModbusExceptionCode? code = null) : Exception(message) +{ + public ModbusExceptionCode? Code { get; } = code; + + public static string DescribeException(byte code) => code switch + { + 0x01 => "Illegal Function", + 0x02 => "Illegal Data Address", + 0x03 => "Illegal Data Value", + 0x04 => "Slave Device Failure", + 0x05 => "Acknowledge", + 0x06 => "Slave Device Busy", + 0x08 => "Memory Parity Error", + 0x0A => "Gateway Path Unavailable", + 0x0B => "Gateway Target Failed To Respond", + _ => $"Unknown Exception 0x{code:X2}" + }; +} + diff --git a/RobotNet.ScriptManager/Connections/UnixDevice.cs b/RobotNet.ScriptManager/Connections/UnixDevice.cs new file mode 100644 index 0000000..de2daee --- /dev/null +++ b/RobotNet.ScriptManager/Connections/UnixDevice.cs @@ -0,0 +1,64 @@ +using RobotNet.Script; + +namespace RobotNet.ScriptManager.Connections; + +public class UnixDevice : IUnixDevice +{ + public static readonly UnixDevice Instance = new(); + + private UnixDevice() { } + public byte[] ReadDev(string name, int length) + { + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + throw new PlatformNotSupportedException("This method is only supported on linux."); + } + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than zero."); + } + + if (string.IsNullOrWhiteSpace(name) || name.Any(c => Path.GetInvalidFileNameChars().Contains(c))) + { + throw new ArgumentException("Invalid device name.", nameof(name)); + } + + var path = $"/dev/robotnet/{name}"; + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Device '{name}' not found.", path); + } + + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + + var buffer = new byte[length]; + var bytesRead = fs.Read(buffer, 0, length); + fs.Close(); + + return buffer; + } + + public void WriteDev(string name, byte[] data) + { + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + throw new PlatformNotSupportedException("This method is only supported on linux."); + } + if (data == null || data.Length == 0) + { + throw new ArgumentNullException(nameof(data), "Data cannot be null or empty."); + } + if (string.IsNullOrWhiteSpace(name) || name.Any(c => Path.GetInvalidFileNameChars().Contains(c))) + { + throw new ArgumentException("Invalid device name.", nameof(name)); + } + var path = $"/dev/robotnet/{name}"; + if (!File.Exists(path)) + { + throw new FileNotFoundException($"Device '{name}' not found.", path); + } + using var fs = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.Read); + fs.Write(data, 0, data.Length); + fs.Close(); + } +} diff --git a/RobotNet.ScriptManager/Controllers/DashboardConfigController.cs b/RobotNet.ScriptManager/Controllers/DashboardConfigController.cs new file mode 100644 index 0000000..6362aae --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/DashboardConfigController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class DashboardConfigController(DashboardConfig Config, ILogger Logger) : ControllerBase +{ + [HttpGet] + public Task> GetOpenACSSettings() + { + try + { + return Task.FromResult>(new(true, "") { Data = Config.MissionNames }); + } + catch (Exception ex) + { + Logger.LogWarning($"Lấy cấu hình OpenACS xảy ra lỗi: {ex.Message}"); + return Task.FromResult>(new(false, "Hệ thống có lỗi xảy ra")); + } + } + + [HttpPost] + public async Task UpdatePublishSetting([FromBody] string[] missionNames) + { + try + { + await Config.UpdateMissionNames(missionNames); + return new(true, ""); + } + catch (Exception ex) + { + Logger.LogWarning($"Cập nhật cấu hình publish OpenACS xảy ra lỗi: {ex.Message}"); + return new(false, "Hệ thống có lỗi xảy ra"); + } + } +} diff --git a/RobotNet.ScriptManager/Controllers/ScriptController.cs b/RobotNet.ScriptManager/Controllers/ScriptController.cs new file mode 100644 index 0000000..7147d6a --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/ScriptController.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Helpers; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ScriptController(ScriptStateManager scriptBuilder) : ControllerBase +{ + [HttpGet] + [Route("")] + public ScriptFolder Gets() => new("Scripts", GetScriptFolders(ScriptConfiguration.ScriptStorePath), GetScriptFiles(ScriptConfiguration.ScriptStorePath)); + + [HttpPost] + [Route("Directory")] + public MessageResult CreateDirectory([FromBody] CreateModel model) + { + if(scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể tạo thư mục"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path, model.Name); + if (Directory.Exists(fullPath)) return new(false, "Folder đã tồn tại"); + try + { + Directory.CreateDirectory(fullPath); + return new(true, ""); + } + catch (Exception e) + { + return new(false, e.Message); + } + } + + [HttpPost] + [Route("File")] + public MessageResult CreateFile([FromBody] CreateModel model) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể tạo file"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path, model.Name); + if (System.IO.File.Exists(fullPath)) return new(false, "File đã tồn tại"); + try + { + var fs = System.IO.File.Create(fullPath); + fs.Close(); + return new(true, ""); + } + catch (Exception e) + { + return new(false, e.Message); + } + } + + [HttpPatch] + [Route("File")] + public MessageResult UpdateCode([FromBody] UpdateModel model) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể update file"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path); + System.IO.File.WriteAllText(fullPath, model.Code); + ResetScriptProcessor(); + return new(true, ""); + } + + + [HttpPatch] + [Route("FileName")] + public MessageResult RenameFile([FromBody] RenameModel model) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể thay đổi tên file"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path); + if (!System.IO.File.Exists(fullPath)) return new(false, "Source code không tồn tại"); + + var folder = Path.GetDirectoryName(fullPath) ?? ""; + var fi = new FileInfo(fullPath); + fi.MoveTo(Path.Combine(folder, model.NewName)); + return new(true, ""); + } + + [HttpPatch] + [Route("DirectoryName")] + public MessageResult RenameDirectory([FromBody] RenameModel model) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể thay đổi tên thư mục"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, model.Path); + if (!Directory.Exists(fullPath)) return new(false, "Folder không tồn tại"); + + var folder = Path.GetDirectoryName(fullPath) ?? ""; + var di = new DirectoryInfo(fullPath); + di.MoveTo(Path.Combine(folder, model.NewName)); + return new(true, ""); + } + + [HttpDelete] + [Route("File")] + public MessageResult DeleteFile([FromQuery] string path) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể xóa file"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, path); + if (!System.IO.File.Exists(fullPath)) return new(false, "Source code không tồn tại"); + + System.IO.File.Delete(fullPath); + ResetScriptProcessor(); + return new(true, ""); + } + + [HttpDelete] + [Route("Directory")] + public MessageResult DeleteDirectory([FromQuery] string path) + { + if (scriptBuilder.State == ProcessorState.Building + || scriptBuilder.State == ProcessorState.Starting + || scriptBuilder.State == ProcessorState.Running + || scriptBuilder.State == ProcessorState.Stopping) + { + return new(false, $"Hệ thống đang trong trạng thái {scriptBuilder.State}, không thể xóa thư mục"); + } + var fullPath = Path.Combine(ScriptConfiguration.ScriptStorePath, path); + if (!Directory.Exists(fullPath)) return new(false, "Folder không tồn tại"); + + var di = new DirectoryInfo(fullPath); + di.Delete(true); + ResetScriptProcessor(); + return new(true, ""); + } + + [HttpGet] + [Route("UsingNamespaces")] + public IEnumerable GetUsingNamespaces() => ScriptConfiguration.UsingNamespaces; + + [HttpGet] + [Route("PreCode")] + public string GetPreCode() => ScriptConfiguration.DeveloptGlobalsScript; + + private void ResetScriptProcessor() + { + string message = string.Empty; + scriptBuilder.Reset(ref message); + } + + private static IEnumerable GetScriptFiles(string parentDir) + { + var dirInfo = new DirectoryInfo(parentDir); + foreach (var fileInfo in dirInfo.GetFiles()) + { + yield return new ScriptFile(fileInfo.Name, System.IO.File.ReadAllText(fileInfo.FullName)); + } + } + + private static IEnumerable GetScriptFolders(string parentDir) + { + var dirInfo = new DirectoryInfo(parentDir); + foreach (var dir in dirInfo.GetDirectories()) + { + yield return new ScriptFolder(dir.Name, GetScriptFolders(dir.FullName), GetScriptFiles(dir.FullName)); + } + } + +} diff --git a/RobotNet.ScriptManager/Controllers/ScriptManagerLoggerController.cs b/RobotNet.ScriptManager/Controllers/ScriptManagerLoggerController.cs new file mode 100644 index 0000000..1940117 --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/ScriptManagerLoggerController.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace RobotNet.ScriptManager.Controllers; +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class ScriptManagerLoggerController(ILogger Logger) : ControllerBase +{ + private readonly string LoggerDirectory = "scriptManagerlogs"; + + [HttpGet] + public async Task> GetLogs([FromQuery(Name = "date")] DateTime date) + { + string temp = ""; + try + { + string fileName = $"{date:yyyy-MM-dd}.log"; + string path = Path.Combine(LoggerDirectory, fileName); + if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory))) + { + Logger.LogWarning($"GetLogs: phát hiện đường dẫn không hợp lệ."); + return []; + } + + if (!System.IO.File.Exists(path)) + { + Logger.LogWarning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}."); + return []; + } + + temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log"); + System.IO.File.Copy(path, temp); + + return await System.IO.File.ReadAllLinesAsync(temp); + } + catch (Exception ex) + { + Logger.LogWarning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}"); + return []; + } + finally + { + if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp); + } + } +} diff --git a/RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs b/RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs new file mode 100644 index 0000000..4048f4c --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/ScriptMissionsController.cs @@ -0,0 +1,154 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Data; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; +using System.Security.Claims; + +namespace RobotNet.ScriptManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ScriptMissionsController(ScriptMissionManager missionManager, ScriptManagerDbContext scriptManagerDb, ScriptMissionCreator missionCreator) : ControllerBase +{ + [HttpGet] + [Route("")] + public IEnumerable GetAlls() => missionManager.GetMissionDatas(); + + [HttpGet] + [Route("Runner")] + public async Task> Search( + [FromQuery(Name = "txtSearch")] string? txtSearch, + [FromQuery(Name = "page")] int page, + [FromQuery(Name = "size")] int size) + { + IOrderedQueryable query; + + if (string.IsNullOrWhiteSpace(txtSearch)) + { + query = scriptManagerDb.InstanceMissions.AsQueryable().OrderByDescending(x => x.CreatedAt); + } + else + { + query = scriptManagerDb.InstanceMissions.AsQueryable().Where(x => x.MissionName.Contains(txtSearch)).OrderByDescending(x => x.CreatedAt); + } + + var total = await query.CountAsync(); + + // Đảm bảo size hợp lệ + if (size <= 0) size = 10; + + // Tính tổng số trang + var totalPages = (int)Math.Ceiling(total / (double)size); + + // Đảm bảo page hợp lệ + if (page <= 0) page = 1; + if (page > totalPages && totalPages > 0) page = totalPages; + + // Nếu không có dữ liệu, trả về rỗng + var items = Enumerable.Empty(); + if (total > 0) + { + items = [.. await query + .OrderByDescending(x => x.CreatedAt) + .Skip((page - 1) * size) + .Take(size) + .Select(x => new InstanceMissionDto + { + Id = x.Id, + MissionName = x.MissionName, + Parameters = x.Parameters, + CreatedAt = x.CreatedAt, + Status = x.Status, + Log = x.Log + }) + .ToListAsync()]; + } + + return new SearchResult + { + Total = total, + Page = page, + Size = size, + Items = items + }; + } + + [HttpPost] + [Route("Runner")] + public async Task> Create(InstanceMissionCreateModel model) + { + try + { + var id = await missionCreator.CreateMissionAsync(model.Name, model.Parameters); + return new(true, "Tạo nhiệm vụ thành công") { Data = id }; + } + catch (Exception ex) + { + return new MessageResult(false, $"Lỗi khi tạo nhiệm vụ: {ex.Message}"); + } + } + + [HttpDelete] + [Route("Runner/{id:guid}")] + public async Task Cancel(Guid id, [FromQuery] string? reason) + { + if (missionManager.Cancel(id, $"Cancel by user '{HttpContext.User.FindFirst(ClaimTypes.Name)?.Value??HttpContext.User.Identity?.Name}' with reason '{reason}'")) + { + var mission = await scriptManagerDb.InstanceMissions.FindAsync(id); + if (mission != null) + { + mission.Status = MissionStatus.Canceling; + await scriptManagerDb.SaveChangesAsync(); + } + return new MessageResult(true, "Gửi yêu cầu hủy nhiệm vụ thành công"); + } + else + { + return new MessageResult(false, "Không thể hủy nhiệm vụ này hoặc nhiệm vụ không tồn tại"); + } + } + + [HttpPut] + [Route("Runner/{id:guid}/pause")] + public async Task Pause(Guid id) + { + if (missionManager.Pause(id)) + { + var mission = await scriptManagerDb.InstanceMissions.FindAsync(id); + if (mission != null) + { + mission.Status = MissionStatus.Pausing; + await scriptManagerDb.SaveChangesAsync(); + } + return new MessageResult(true, "Gửi yêu cầu tạm dừng nhiệm vụ thành công"); + } + else + { + return new MessageResult(false, "Không thể tạm dừng nhiệm vụ này hoặc nhiệm vụ không tồn tại"); + } + } + + [HttpPut] + [Route("Runner/{id:guid}/resume")] + public async Task Resume(Guid id) + { + if (missionManager.Resume(id)) + { + var mission = await scriptManagerDb.InstanceMissions.FindAsync(id); + if (mission != null) + { + mission.Status = MissionStatus.Resuming; + await scriptManagerDb.SaveChangesAsync(); + } + return new MessageResult(true, "Gửi yêu cầu tiếp tục nhiệm vụ thành công"); + } + else + { + return new MessageResult(false, "Không thể tiếp tục nhiệm vụ này hoặc nhiệm vụ không tồn tại"); + } + } +} diff --git a/RobotNet.ScriptManager/Controllers/ScriptTasksController.cs b/RobotNet.ScriptManager/Controllers/ScriptTasksController.cs new file mode 100644 index 0000000..1b7021f --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/ScriptTasksController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ScriptTasksController(ScriptTaskManager taskManager) : ControllerBase +{ + [HttpGet] + [Route("")] + public IEnumerable GetAlls() => taskManager.GetTaskDatas(); + +} diff --git a/RobotNet.ScriptManager/Controllers/ScriptVariablesController.cs b/RobotNet.ScriptManager/Controllers/ScriptVariablesController.cs new file mode 100644 index 0000000..21a7bec --- /dev/null +++ b/RobotNet.ScriptManager/Controllers/ScriptVariablesController.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Controllers; + +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ScriptVariablesController(ScriptGlobalsManager globalManager) : ControllerBase +{ + [HttpGet] + [Route("")] + public IEnumerable GetAlls() => globalManager.GetVariablesData(); + + + [HttpGet] + [Route("{name}")] + public MessageResult GetVariableValue(string name) + { + if (globalManager.Globals.TryGetValue(name, out var value)) + { + return new (true, "") { Data = value?.ToString() ?? "null" }; + } + else + { + return new(false, $"Variable \"{name}\" not found."); + } + } + + [HttpPut] + [Route("{name}")] + public MessageResult UpdateVariableValue(string name, [FromBody] UpdateVariableModel model) + { + try + { + if(name != model.Name) + { + return new MessageResult(false, "Variable name in the URL does not match the name in the body."); + } + + globalManager.SetValue(model.Name, model.Value); + return new MessageResult(true, $"Variable \"{model.Name}\" updated {model.Value} successfully."); + } + catch (Exception ex) + { + return new MessageResult(false, ex.Message); + } + } + + [HttpPut] + [Route("{name}/Reset")] + public MessageResult ResetVariableValue(string name, [FromBody] UpdateVariableModel model) + { + try + { + if (name != model.Name) + { + return new MessageResult(false, "Variable name in the URL does not match the name in the body."); + } + + globalManager.ResetValue(model.Name); + return new MessageResult(true, $"Variable \"{model.Name}\" reset successfully."); + } + catch (Exception ex) + { + return new MessageResult(false, ex.Message); + } + } +} diff --git a/RobotNet.ScriptManager/Data/InstanceMission.cs b/RobotNet.ScriptManager/Data/InstanceMission.cs new file mode 100644 index 0000000..645d8b8 --- /dev/null +++ b/RobotNet.ScriptManager/Data/InstanceMission.cs @@ -0,0 +1,39 @@ +using RobotNet.Script.Shares; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace RobotNet.ScriptManager.Data; + +#nullable disable + +[Table("InstanceMissions")] +public class InstanceMission +{ + [Column("Id", TypeName = "uniqueidentifier")] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + [Required] + public Guid Id { get; set; } + + [Column("MissionName", TypeName = "varchar(126)")] + [Required] + public string MissionName { get; set; } + + [Column("CreatedAt", TypeName = "datetime2")] + [Required] + public DateTime CreatedAt { get; set; } + + [Column("Parameters", TypeName = "nvarchar(max)")] + public string Parameters { get; set; } + + [Column("Status", TypeName = "int")] + [Required] + public MissionStatus Status { get; set; } + + [Column("StopedAt", TypeName = "datetime2")] + [Required] + public DateTime StopedAt { get; set; } + + [Column("Log", TypeName = "nvarchar(max)")] + public string Log { get; set; } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs b/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs new file mode 100644 index 0000000..2f6bcfa --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.Designer.cs @@ -0,0 +1,61 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.ScriptManager.Data; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + [DbContext(typeof(ScriptManagerDbContext))] + [Migration("20250630100458_InitScriptManagerDbContext")] + partial class InitScriptManagerDbContext + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Log") + .HasColumnType("nvarchar(max)") + .HasColumnName("Log"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("varchar(126)") + .HasColumnName("MissionName"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("Status"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("InstanceMissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.cs b/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.cs new file mode 100644 index 0000000..98e17f7 --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250630100458_InitScriptManagerDbContext.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + /// + public partial class InitScriptManagerDbContext : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "InstanceMissions", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + MissionName = table.Column(type: "varchar(126)", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + Status = table.Column(type: "int", nullable: false), + Log = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_InstanceMissions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_InstanceMissions_CreatedAt", + table: "InstanceMissions", + column: "CreatedAt"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "InstanceMissions"); + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs b/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs new file mode 100644 index 0000000..e1c1bf6 --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.Designer.cs @@ -0,0 +1,65 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.ScriptManager.Data; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + [DbContext(typeof(ScriptManagerDbContext))] + [Migration("20250701130724_AddParametersToInstanceMission")] + partial class AddParametersToInstanceMission + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Log") + .HasColumnType("nvarchar(max)") + .HasColumnName("Log"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("varchar(126)") + .HasColumnName("MissionName"); + + b.Property("Parameters") + .HasColumnType("nvarchar(max)") + .HasColumnName("Parameters"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("Status"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("InstanceMissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.cs b/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.cs new file mode 100644 index 0000000..83d6fb1 --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250701130724_AddParametersToInstanceMission.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + /// + public partial class AddParametersToInstanceMission : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Parameters", + table: "InstanceMissions", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Parameters", + table: "InstanceMissions"); + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs b/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs new file mode 100644 index 0000000..ec7714d --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.Designer.cs @@ -0,0 +1,69 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.ScriptManager.Data; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + [DbContext(typeof(ScriptManagerDbContext))] + [Migration("20250814072909_AddStopedAtToInstanceMission")] + partial class AddStopedAtToInstanceMission + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Log") + .HasColumnType("nvarchar(max)") + .HasColumnName("Log"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("varchar(126)") + .HasColumnName("MissionName"); + + b.Property("Parameters") + .HasColumnType("nvarchar(max)") + .HasColumnName("Parameters"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("Status"); + + b.Property("StopedAt") + .HasColumnType("datetime2") + .HasColumnName("StopedAt"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("InstanceMissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.cs b/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.cs new file mode 100644 index 0000000..8e587a6 --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/20250814072909_AddStopedAtToInstanceMission.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + /// + public partial class AddStopedAtToInstanceMission : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "StopedAt", + table: "InstanceMissions", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "StopedAt", + table: "InstanceMissions"); + } + } +} diff --git a/RobotNet.ScriptManager/Data/Migrations/ScriptManagerDbContextModelSnapshot.cs b/RobotNet.ScriptManager/Data/Migrations/ScriptManagerDbContextModelSnapshot.cs new file mode 100644 index 0000000..97bd443 --- /dev/null +++ b/RobotNet.ScriptManager/Data/Migrations/ScriptManagerDbContextModelSnapshot.cs @@ -0,0 +1,66 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RobotNet.ScriptManager.Data; + +#nullable disable + +namespace RobotNet.ScriptManager.Data.Migrations +{ + [DbContext(typeof(ScriptManagerDbContext))] + partial class ScriptManagerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("RobotNet.ScriptManager.Data.InstanceMission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("Id"); + + b.Property("CreatedAt") + .HasColumnType("datetime2") + .HasColumnName("CreatedAt"); + + b.Property("Log") + .HasColumnType("nvarchar(max)") + .HasColumnName("Log"); + + b.Property("MissionName") + .IsRequired() + .HasColumnType("varchar(126)") + .HasColumnName("MissionName"); + + b.Property("Parameters") + .HasColumnType("nvarchar(max)") + .HasColumnName("Parameters"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("Status"); + + b.Property("StopedAt") + .HasColumnType("datetime2") + .HasColumnName("StopedAt"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("InstanceMissions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs b/RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs new file mode 100644 index 0000000..3fa7bc6 --- /dev/null +++ b/RobotNet.ScriptManager/Data/ScriptManagerDbContext.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.ScriptManager.Data; + +public class ScriptManagerDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet InstanceMissions { get; private set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasIndex(im => im.CreatedAt); + } +} diff --git a/RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs b/RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs new file mode 100644 index 0000000..a9ba7b1 --- /dev/null +++ b/RobotNet.ScriptManager/Data/ScriptManagerDbExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; + +namespace RobotNet.ScriptManager.Data; + +public static class ScriptManagerDbExtensions +{ + public static async Task SeedScriptManagerDbAsync(this IServiceProvider serviceProvider) + { + using var scope = serviceProvider.GetRequiredService().CreateScope(); + + using var appDb = scope.ServiceProvider.GetRequiredService(); + + await appDb.Database.MigrateAsync(); + //await appDb.Database.EnsureCreatedAsync(); + await appDb.SaveChangesAsync(); + } +} diff --git a/RobotNet.ScriptManager/Dockerfile b/RobotNet.ScriptManager/Dockerfile new file mode 100644 index 0000000..7a81259 --- /dev/null +++ b/RobotNet.ScriptManager/Dockerfile @@ -0,0 +1,105 @@ +FROM alpine:3.22 AS base +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["RobotNet.ScriptManager/RobotNet.ScriptManager.csproj", "RobotNet.ScriptManager/"] +COPY ["RobotNet.Script.Shares/RobotNet.Script.Shares.csproj", "RobotNet.Script.Shares/"] +COPY ["RobotNet.RobotShares/RobotNet.RobotShares.csproj", "RobotNet.RobotShares/"] +COPY ["RobotNet.MapShares/RobotNet.MapShares.csproj", "RobotNet.MapShares/"] +COPY ["RobotNet.Script/RobotNet.Script.csproj", "RobotNet.Script/"] +COPY ["RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj", "RobotNet.Script.Expressions/"] +COPY ["RobotNet.Shares/RobotNet.Shares.csproj", "RobotNet.Shares/"] +COPY ["RobotNet.OpenIddictClient/RobotNet.OpenIddictClient.csproj", "RobotNet.OpenIddictClient/"] +COPY ["RobotNet.Clients/RobotNet.Clients.csproj", "RobotNet.Clients/"] + +# RUN dotnet package remove "Microsoft.EntityFrameworkCore.Tools" --project "RobotNet.ScriptManager/RobotNet.ScriptManager.csproj" +RUN dotnet restore "RobotNet.ScriptManager/RobotNet.ScriptManager.csproj" + +COPY RobotNet.Script/ RobotNet.Script/ +COPY RobotNet.Script.Expressions/ RobotNet.Script.Expressions/ +RUN rm -rf ./RobotNet.Script/bin +RUN rm -rf ./RobotNet.Script/obj +RUN rm -rf ./RobotNet.Script.Expressions/bin +RUN rm -rf ./RobotNet.Script.Expressions/obj + +WORKDIR "/src/RobotNet.Script" +RUN dotnet build "RobotNet.Script.csproj" -c Release -o /app/script/ + +WORKDIR /src + +COPY RobotNet.ScriptManager/ RobotNet.ScriptManager/ +COPY RobotNet.Script.Shares/ RobotNet.Script.Shares/ +COPY RobotNet.RobotShares/ RobotNet.RobotShares/ +COPY RobotNet.MapShares/ RobotNet.MapShares/ +COPY RobotNet.Script/ RobotNet.Script/ +COPY RobotNet.Shares/ RobotNet.Shares/ +COPY RobotNet.OpenIddictClient/ RobotNet.OpenIddictClient/ +COPY RobotNet.Clients/ RobotNet.Clients/ + +RUN rm -rf ./RobotNet.ScriptManager/bin +RUN rm -rf ./RobotNet.ScriptManager/obj +RUN rm -rf ./RobotNet.Script.Shares/bin +RUN rm -rf ./RobotNet.Script.Shares/obj +RUN rm -rf ./RobotNet.RobotShares/bin +RUN rm -rf ./RobotNet.RobotShares/obj +RUN rm -rf ./RobotNet.MapShares/bin +RUN rm -rf ./RobotNet.MapShares/obj +RUN rm -rf ./RobotNet.Script/bin +RUN rm -rf ./RobotNet.Script/obj +RUN rm -rf ./RobotNet.Shares/bin +RUN rm -rf ./RobotNet.Shares/obj +RUN rm -rf ./RobotNet.OpenIddictClient/bin +RUN rm -rf ./RobotNet.OpenIddictClient/obj +RUN rm -rf ./RobotNet.Clients/bin +RUN rm -rf ./RobotNet.Clients/obj + +RUN rm /src/RobotNet.ScriptManager/wwwroot/dlls/* +RUN cp /app/script/RobotNet.Script.dll /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /app/script/RobotNet.Script.xml /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /app/script/RobotNet.Script.Expressions.dll /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /app/script/RobotNet.Script.Expressions.xml /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Collections.dll /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Linq.Expressions.dll /src/RobotNet.ScriptManager/wwwroot/dlls/ +RUN cp /usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/$DOTNET_VERSION/ref/net9.0/System.Runtime.dll /src/RobotNet.ScriptManager/wwwroot/dlls/ + +WORKDIR "/src/RobotNet.ScriptManager" +RUN dotnet build "RobotNet.ScriptManager.csproj" -c Release -o /app/build + +FROM build AS publish +WORKDIR /src/RobotNet.ScriptManager +RUN dotnet publish "RobotNet.ScriptManager.csproj" \ + -c Release \ + -o /app/publish \ + --runtime linux-musl-x64 \ + --self-contained true \ + /p:PublishTrimmed=false \ + /p:PublishReadyToRun=true + +WORKDIR /app/publish +RUN mkdir -p /app/publish/scripts +RUN mkdir -p /app/publish/dlls +RUN cp /app/build/RobotNet.Script.dll /app/publish/dlls/RobotNet.Script.dll +RUN cp /app/build/RobotNet.Script.Expressions.dll /app/publish/dlls/RobotNet.Script.Expressions.dll +RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Linq.Expressions.dll /app/publish/dlls/ +RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Private.CoreLib.dll /app/publish/dlls/ +RUN cp /usr/share/dotnet/shared/Microsoft.NETCore.App/$DOTNET_VERSION/System.Runtime.dll /app/publish/dlls/ + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish /app/ + +RUN apk add --no-cache icu-libs tzdata ca-certificates + +RUN echo '#!/bin/sh' >> ./start.sh +RUN echo 'update-ca-certificates' >> ./start.sh +RUN echo 'cd /app' >> ./start.sh +RUN echo 'exec ./RobotNet.ScriptManager' >> ./start.sh + +RUN chmod +x ./RobotNet.ScriptManager +RUN chmod +x ./start.sh + +# Use the start script to ensure certificates are updated before starting the application +EXPOSE 443 +ENTRYPOINT ["./start.sh"] diff --git a/RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs b/RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs new file mode 100644 index 0000000..4b6fb57 --- /dev/null +++ b/RobotNet.ScriptManager/Helpers/CSharpSyntaxHelper.cs @@ -0,0 +1,707 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using RobotNet.Script; +using RobotNet.ScriptManager.Models; +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; + +namespace RobotNet.ScriptManager.Helpers; + +public static class CSharpSyntaxHelper +{ + public static bool ResolveValueFromString(string valueStr, Type type, out object? value) + { + // Check if type is in MissionParameterTypes + if (!MissionParameterTypes.Contains(type)) + { + value = null; + return false; + } + + // If type is RobotNet.Script.IRobot, return null + if (type == RobotType) + { + value = null; + return true; + } + + // Convert string to the corresponding type + if (type == typeof(string)) + { + value = valueStr; + return true; + } + + if (type.IsEnum) + { + value = Enum.Parse(type, valueStr, ignoreCase: true); + return true; + } + + // Handle nullable types + var underlyingType = Nullable.GetUnderlyingType(type); + if (underlyingType != null) + { + value = null; + return false; + } + + value = Convert.ChangeType(valueStr, type); + if (value is null) + { + return false; + } + else + { + return true; + } + } + public static bool VerifyScript(string code, out string error, + out List variables, + out List tasks, + out List missions) + { + try + { + var listVariables = new List(); + var wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", ScriptConfiguration.DeveloptGlobalsScript, code, "}"]); + + var devCompilation = CSharpCompilation.Create("CodeAnalysis") + .WithReferences(ScriptConfiguration.MetadataReferences) + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddSyntaxTrees(CSharpSyntaxTree.ParseText(wrappedCode)); + + var diagnostics = devCompilation.GetDiagnostics(); + if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) + { + var message = "Verify errors: \r\n"; + foreach (var diag in diagnostics) + { + if (diag.Severity == DiagnosticSeverity.Error) + { + message += $"\t❌ Error: {diag.GetMessage()} at {diag.Location}\r\n"; + } + else if (diag.Severity == DiagnosticSeverity.Warning) + { + message += $"\t⚠️ Warning: {diag.GetMessage()} at {diag.Location}\r\n"; + } + } + throw new Exception(message); + } + + error = ""; + wrappedCode = string.Join(Environment.NewLine, [ScriptConfiguration.UsingNamespacesScript, "public class DummyClass", "{", code, "}"]); + var syntaxTree = CSharpSyntaxTree.ParseText(wrappedCode); + var root = syntaxTree.GetCompilationUnitRoot(); + + var runCompilation = CSharpCompilation.Create("CodeAnalysis") + .AddSyntaxTrees(syntaxTree) + .WithReferences(ScriptConfiguration.MetadataReferences) + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var classNode = root.DescendantNodes().OfType().FirstOrDefault(c => c.Identifier.Text.Equals("DummyClass")) ?? throw new Exception("No class named 'DummyClass' found in the script."); + var semanticModel = runCompilation.GetSemanticModel(syntaxTree); + GetScriptVariables(classNode, semanticModel, out variables); + GetScriptTasksAndMissions(classNode, semanticModel, out tasks, out missions); + + return true; + } + catch (Exception ex) + { + error = $"An error occurred while verifying the script: {ex.Message}"; + variables = []; + tasks = []; + missions = []; + return false; + } + finally + { + GC.Collect(); + } + } + + private static void GetScriptVariables(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List variables) + { + variables = []; + var fields = classNode.Members.OfType(); + foreach (var field in fields) + { + Type resolvedType = semanticModel.ToSystemType(field.Declaration.Type) ?? throw new Exception($"Failed to resolve type for field: {field.Declaration.Type.ToFullString()}"); + // Check if the field has VariableAttribute + + VariableAttribute? varAttr = null; + foreach (var attrList in field.AttributeLists) + { + foreach (var attr in attrList.Attributes) + { + var attrType = semanticModel.GetTypeAttribute(attr); + if (attrType == VariableAttributeType) + { + varAttr = semanticModel.GetConstantAttribute(attr, attrType) as RobotNet.Script.VariableAttribute; + break; + } + } + if (varAttr != null) break; + } + + foreach (var variable in field.Declaration.Variables) + { + var name = variable.Identifier.Text; + if (string.IsNullOrEmpty(name)) continue; + + + + if (variable.Initializer is null) + { + var value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null; + variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false)); + } + else + { + var constant = semanticModel.GetConstantValue(variable.Initializer.Value); + if (constant.HasValue) + { + try + { + var value = Convert.ChangeType(constant.Value, resolvedType); + variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false)); + } + catch (Exception ex) + { + throw new Exception($"Failed to convert value of {name} = \"{constant.Value}\" to {resolvedType}: {ex}"); + } + } + else + { + var code = variable.Initializer.Value.ToFullString(); + object? value; + if (string.IsNullOrEmpty(code)) + { + value = resolvedType.IsValueType ? Activator.CreateInstance(resolvedType) : null; + } + else + { + value = CSharpScript.EvaluateAsync(code, ScriptConfiguration.ScriptOptions).GetAwaiter().GetResult(); + } + variables.Add(new ScriptVariableSyntax(name, resolvedType, value, varAttr != null, varAttr?.PublicWrite ?? false)); + } + } + } + } + + // TODO: kiểm tra các properties là auto-property có đủ và getter và setter thì add vào variables + var properties = classNode.Members.OfType(); + foreach (var prop in properties) + { + // Kiểm tra có cả getter và setter + var accessors = prop.AccessorList?.Accessors.ToList() ?? []; + bool hasGetter = accessors?.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration) == true; + bool hasSetter = accessors?.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration) == true; + + // Kiểm tra auto-property: cả getter và setter đều không có body và không phải expression-bodied + bool isAutoProperty = hasGetter && hasSetter && + accessors!.All(a => a.Body == null && a.ExpressionBody == null) && + prop.ExpressionBody == null; + + if (isAutoProperty) + { + var name = prop.Identifier.Text; + var type = semanticModel.ToSystemType(prop.Type) ?? throw new Exception($"Failed to resolve type for property: {prop.Type.ToFullString()}"); + // Giá trị mặc định của auto-property là default(T) + object? value = type.IsValueType ? Activator.CreateInstance(type) : null; + variables.Add(new ScriptVariableSyntax(name, type, value, false, false)); + } + } + } + + private static void GetScriptTasksAndMissions(ClassDeclarationSyntax classNode, SemanticModel semanticModel, out List tasks, out List missions) + { + tasks = []; + missions = []; + var methods = classNode.Members.OfType(); + foreach (var method in methods) + { + bool attrDone = false; + foreach (var attrList in method.AttributeLists) + { + foreach (var attr in attrList.Attributes) + { + var attrType = semanticModel.GetTypeAttribute(attr); + if (attrType == TaskAttributeType) + { + attrDone = true; + + if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.TaskAttribute taskAttr) + { + throw new Exception($"Failed to get TaskAttribute from method {method.Identifier.Text}"); + } + + // Check if method returns Task or Task + var returnType = semanticModel.ToSystemType(method.ReturnType); + bool isTask = returnType == typeof(System.Threading.Tasks.Task) || + (returnType != null && returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(System.Threading.Tasks.Task<>)); + bool isVoid = returnType == typeof(void); + + if (method.ParameterList.Parameters.Count > 0 || !(isTask || isVoid)) + { + throw new Exception($"Task Method {method.Identifier.Text} with TaskAttribute must have no parameters and return type void or Task."); + } + + tasks.Add(new ScriptTaskData(method.Identifier.Text, + taskAttr.Interval, + taskAttr.AutoStart, + method.ToFullString(), + ExtractRelatedCodeForScriptRunner(classNode, method, $"{(isTask ? "await " : "")}{method.Identifier.Text}();"))); + break; + } + else if (attrType == MisionAttributeType) + { + attrDone = true; + if (semanticModel.GetConstantAttribute(attr, attrType) is not RobotNet.Script.MissionAttribute missionAttr) + { + throw new Exception($"Failed to get MissionAttribute from method {method.Identifier.Text}"); + } + var returnType = semanticModel.ToSystemType(method.ReturnType); + if (returnType is null || returnType != MisionReturnType) + { + throw new Exception($"Mission Method {method.Identifier.Text} with MissionAttribute must return type IEnumerator."); + } + + var inputParameters = new List(); + var parameters = new List(); + bool hasCancellationTokenParameter = false; + foreach (var param in method.ParameterList.Parameters) + { + if (param.Type is null) + { + throw new Exception($"Parameter {param.Identifier.Text} in method {method.Identifier.Text} has no type specified."); + } + + var paramType = semanticModel.ToSystemType(param.Type) ?? throw new Exception($"Failed to resolve type for parameter {param.Identifier.Text} in method {method.Identifier.Text}"); + if (!MissionParameterTypes.Contains(paramType)) + { + throw new Exception($"Parameter type {param.Type} {param.Identifier.Text} in method mission {method.Identifier.Text} is not supported"); + } + + if (paramType == typeof(CancellationToken)) + { + if (hasCancellationTokenParameter) + { + throw new Exception($"Method {method.Identifier.Text} has multiple CancellationToken parameters, which is not allowed."); + } + hasCancellationTokenParameter = true; + } + + // lấy default value nếu có + object? defaultValue = null; + if (param.Default is EqualsValueClauseSyntax equalsValue) + { + var constValue = semanticModel.GetConstantValue(equalsValue.Value); + if (constValue.HasValue) + { + defaultValue = constValue.Value; + } + } + + //inputParameters.Add($@"({paramType.FullName})parameters["""+param.Identifier.Text+"""]"); + inputParameters.Add($@"({paramType.FullName})parameters[""{param.Identifier.Text}""]"); + parameters.Add(new ScriptMissionParameter(param.Identifier.Text, paramType, defaultValue)); + } + + var execScript = $"return {method.Identifier.Text}({string.Join(", ", inputParameters)});"; + missions.Add(new ScriptMissionData(method.Identifier.Text, + parameters, + method.ToFullString(), + ExtractRelatedCodeForScriptRunner(classNode, method, execScript), + missionAttr.IsMultipleRun)); + break; + } + + } + if (attrDone) break; + } + } + } + + private static string ExtractRelatedCodeForScriptRunner(ClassDeclarationSyntax classNode, MethodDeclarationSyntax rootMethod, string execScript) + { + var allMethods = classNode.Members.OfType().ToList(); + var allFields = classNode.Members.OfType().ToList(); + var allProperties = classNode.Members.OfType().ToList(); + var allNestedTypes = classNode.Members + .Where(m => m is ClassDeclarationSyntax || m is StructDeclarationSyntax || m is InterfaceDeclarationSyntax || m is EnumDeclarationSyntax) + .ToList(); + + // 1. BFS: method + non-auto-property + var usedMethodNames = new HashSet(); + var usedPropertyNames = new HashSet(); + var methodQueue = new Queue(); + var collectedMethods = new List(); + var collectedNonAutoProperties = new List(); + + methodQueue.Enqueue(rootMethod); + + while (methodQueue.Count > 0) + { + var member = methodQueue.Dequeue(); + if (member is MethodDeclarationSyntax method) + { + if (!usedMethodNames.Add(method.Identifier.Text)) + continue; + collectedMethods.Add(method); + + // Tìm các method/property được gọi trong method này + var invokedNames = method.DescendantNodes() + .OfType() + .Select(id => id.Identifier.Text) + .Distinct(); + + foreach (var name in invokedNames) + { + // Method + var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name); + if (nextMethod != null && !usedMethodNames.Contains(name)) + methodQueue.Enqueue(nextMethod); + + // Property + var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name); + if (nextProp != null && !usedPropertyNames.Contains(name)) + methodQueue.Enqueue(nextProp); + } + } + else if (member is PropertyDeclarationSyntax prop) + { + if (!usedPropertyNames.Add(prop.Identifier.Text)) + continue; + + // Auto-property: bỏ qua, sẽ xử lý sau + var accessors = prop.AccessorList?.Accessors.ToList() ?? []; + bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration); + bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration); + bool isAutoProperty = hasGetter && hasSetter && + accessors.All(a => a.Body == null && a.ExpressionBody == null) && + prop.ExpressionBody == null; + + if (isAutoProperty) + continue; + + collectedNonAutoProperties.Add(prop); + + // Tìm các method/property/field được gọi trong property này + var invokedNames = prop.DescendantNodes() + .OfType() + .Select(id => id.Identifier.Text) + .Distinct(); + + foreach (var name in invokedNames) + { + // Method + var nextMethod = allMethods.FirstOrDefault(m => m.Identifier.Text == name); + if (nextMethod != null && !usedMethodNames.Contains(name)) + methodQueue.Enqueue(nextMethod); + + // Property + var nextProp = allProperties.FirstOrDefault(p => p.Identifier.Text == name); + if (nextProp != null && !usedPropertyNames.Contains(name)) + methodQueue.Enqueue(nextProp); + } + } + } + + // 2. Collect all used member names (from all collected methods & non-auto-properties) + var usedMemberNames = new HashSet(); + foreach (var method in collectedMethods) + { + foreach (var id in method.DescendantNodes().OfType().Select(id => id.Identifier.Text)) + usedMemberNames.Add(id); + } + foreach (var prop in collectedNonAutoProperties) + { + foreach (var id in prop.DescendantNodes().OfType().Select(id => id.Identifier.Text)) + usedMemberNames.Add(id); + } + + // 3. Collect fields + var relatedFields = new List(); + foreach (var field in allFields) + { + foreach (var variable in field.Declaration.Variables) + { + if (usedMemberNames.Contains(variable.Identifier.Text)) + { + var varType = field.Declaration.Type.ToString(); + var varName = variable.Identifier.Text; + var propertyCode = $@"public {varType} {varName} +{{ + get => ({varType})globals[""{varName}""]; + set => globals[""{varName}""] = value; +}}"; + relatedFields.Add(propertyCode.Trim()); + } + } + } + + // 4. Collect auto-properties + var relatedAutoProperties = new List(); + foreach (var prop in allProperties) + { + var accessors = prop.AccessorList?.Accessors.ToList() ?? []; + bool hasGetter = accessors.Any(a => a.Kind() == SyntaxKind.GetAccessorDeclaration); + bool hasSetter = accessors.Any(a => a.Kind() == SyntaxKind.SetAccessorDeclaration); + bool isAutoProperty = hasGetter && hasSetter && + accessors.All(a => a.Body == null && a.ExpressionBody == null) && + prop.ExpressionBody == null; + + if (isAutoProperty && (usedMemberNames.Contains(prop.Identifier.Text) || usedPropertyNames.Contains(prop.Identifier.Text))) + { + var propType = prop.Type.ToString(); + var propName = prop.Identifier.Text; + var propertyCode = $@"public {propType} {propName} +{{ + get => ({propType})globals[""{propName}""]; + set => globals[""{propName}""] = value; +}}"; + relatedAutoProperties.Add(propertyCode.Trim()); + } + } + + // 5. Collect nested types if referenced + var usedNestedTypeNames = new HashSet(usedMemberNames); + var relatedNestedTypes = allNestedTypes + .Where(nt => + { + if (nt is BaseTypeDeclarationSyntax btd) + return usedNestedTypeNames.Contains(btd.Identifier.Text); + return false; + }) + .Select(nt => nt.NormalizeWhitespace().ToFullString()) + .ToList(); + + // 6. Compose the script + var sb = new StringBuilder(); + sb.AppendLine(ScriptConfiguration.RuntimeGlobalsScript); + foreach (var nt in relatedNestedTypes) + sb.AppendLine(nt); + foreach (var f in relatedFields) + sb.AppendLine(f); + foreach (var p in relatedAutoProperties) + sb.AppendLine(p); + foreach (var p in collectedNonAutoProperties) + sb.AppendLine(p.NormalizeWhitespace().ToFullString()); + foreach (var m in collectedMethods) + sb.AppendLine(m.NormalizeWhitespace().ToFullString()); + + sb.AppendLine(execScript); + return sb.ToString(); + } + + private static Type? GetTypeAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax) + { + var typeInfo = semanticModel.GetTypeInfo(attrSynctax); + if (typeInfo.Type is null) return null; + + string metadataName = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + return ResolveTypeFromString(metadataName); + } + + private static object? GetConstantAttribute(this SemanticModel semanticModel, AttributeSyntax attrSynctax, Type type) + { + var args = new List(); + + foreach (var arg in attrSynctax.ArgumentList?.Arguments ?? default) + { + var constValue = semanticModel.GetConstantValue(arg.Expression); + if (constValue.HasValue) + args.Add(constValue.Value); + else + args.Add(null); // fallback nếu không phân giải được + } + + // Find the constructor with the same number or more parameters (with optional) + var ctors = type.GetConstructors(); + foreach (var ctor in ctors) + { + var parameters = ctor.GetParameters(); + if (args.Count <= parameters.Length) + { + // Fill missing optional parameters with their default values + var finalArgs = args.ToList(); + for (int i = args.Count; i < parameters.Length; i++) + { + if (parameters[i].IsOptional) + finalArgs.Add(parameters[i].DefaultValue); + else + goto NextCtor; // Not enough arguments and not optional + } + return ctor.Invoke([.. finalArgs]); + } + NextCtor:; + } + + return null; + } + + private static Type? ToSystemType(this SemanticModel semanticModel, TypeSyntax typeSyntax) + { + var typeSymbol = semanticModel.GetTypeInfo(typeSyntax).Type; + if (typeSymbol is null) return null; + + if (typeSyntax is PredefinedTypeSyntax predefinedType + && predefinedMap.TryGetValue(predefinedType.Keyword.Text, out var systemType)) + { + return systemType; + } + + string metadataName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + return metadataName.Equals("void") ? typeof(void) : ResolveTypeFromString(metadataName); + } + + private static Type? ResolveTypeFromString(string typeString) + { + typeString = typeString.Trim(); + + // Handle array types (e.g., System.Int32[], string[], List[]) + if (typeString.EndsWith("[]")) + { + var elementTypeString = typeString[..^2].Trim(); + var elementType = ResolveTypeFromString(elementTypeString); + return elementType?.MakeArrayType(); + } + + // Trường hợp không phải generic + if (!typeString.Contains('<')) + { + return FindType(typeString); + } + + // Tách phần generic + var match = Regex.Match(typeString, @"^(?[^<]+)<(?.+)>$"); + if (!match.Success) + return null; + + var genericTypeName = match.Groups["raw"].Value; + var genericArgsString = match.Groups["args"].Value; + + // Phân tách các generic argument (xử lý nested generics) + var genericArgs = SplitGenericArguments(genericArgsString); + + var genericType = FindType(genericTypeName + "`" + genericArgs.Count); + if (genericType == null) + return null; + + var resolvedArgs = genericArgs.Select(ResolveTypeFromString).ToArray(); + if (resolvedArgs.Any(t => t == null)) + return null; + + return genericType.MakeGenericType(resolvedArgs!); + } + + private static List SplitGenericArguments(string input) + { + var args = new List(); + var sb = new StringBuilder(); + int depth = 0; + + foreach (char c in input) + { + if (c == ',' && depth == 0) + { + args.Add(sb.ToString().Trim()); + sb.Clear(); + } + else + { + if (c == '<') depth++; + else if (c == '>') depth--; + sb.Append(c); + } + } + + if (sb.Length > 0) + { + args.Add(sb.ToString().Trim()); + } + + return args; + } + + private static Type? FindType(string typeName) + { + if (predefinedMap.TryGetValue(typeName, out var systemType)) return systemType; + + return AppDomain.CurrentDomain + .GetAssemblies() + .Select(a => a.GetType(typeName, false)) + .FirstOrDefault(t => t != null); + } + + + public static string ToTypeString(Type type) + { + if (type == typeof(void)) + return "void"; + + if (type.IsGenericType) + { + var genericTypeName = type.GetGenericTypeDefinition().FullName; + if (genericTypeName == null) + return type.Name; + + var backtickIndex = genericTypeName.IndexOf('`'); + if (backtickIndex > 0) + genericTypeName = genericTypeName.Substring(0, backtickIndex); + + var genericArgs = type.GetGenericArguments(); + var argsString = string.Join(", ", genericArgs.Select(ToTypeString)); + return $"{genericTypeName}<{argsString}>"; + } + + return type.FullName ?? type.Name; + } + + private static readonly Dictionary predefinedMap = new() + { + ["bool"] = typeof(bool), + ["byte"] = typeof(byte), + ["sbyte"] = typeof(sbyte), + ["short"] = typeof(short), + ["ushort"] = typeof(ushort), + ["int"] = typeof(int), + ["uint"] = typeof(uint), + ["long"] = typeof(long), + ["ulong"] = typeof(ulong), + ["float"] = typeof(float), + ["double"] = typeof(double), + ["decimal"] = typeof(decimal), + ["char"] = typeof(char), + ["string"] = typeof(string), + ["object"] = typeof(object) + }; + + private static readonly Type TaskAttributeType = typeof(RobotNet.Script.TaskAttribute); + private static readonly Type MisionAttributeType = typeof(RobotNet.Script.MissionAttribute); + private static readonly Type MisionReturnType = typeof(IAsyncEnumerable); + private static readonly Type VariableAttributeType = typeof(VariableAttribute); + + private static readonly Type RobotType = typeof(RobotNet.Script.IRobot); + private static readonly ImmutableArray MissionParameterTypes = [ + typeof(bool), + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + typeof(char), + typeof(string), + typeof(CancellationToken), + ]; +} diff --git a/RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs b/RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs new file mode 100644 index 0000000..21bb258 --- /dev/null +++ b/RobotNet.ScriptManager/Helpers/ScriptConfiguration.cs @@ -0,0 +1,226 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Scripting; +using RobotNet.Script.Shares; +using System.Collections.Immutable; +using System.Reflection; +using System.Text; + +namespace RobotNet.ScriptManager.Helpers; + +public static class ScriptConfiguration +{ + public static readonly string ScriptStorePath; + public static readonly string DllsPath; + public static readonly ScriptOptions ScriptOptions; + public static readonly ImmutableArray MetadataReferences; + public static readonly ImmutableArray UsingNamespaces; + public static readonly string UsingNamespacesScript; + public static readonly string DeveloptGlobalsScript; + public static readonly string RuntimeGlobalsScript; + + static ScriptConfiguration() + { + ScriptStorePath = "scripts"; + DllsPath = "dlls"; + UsingNamespaces = ["System", "System.Collections.Generic", "System.Linq.Expressions", "System.Threading", "System.Threading.Tasks", "System.Runtime.CompilerServices", "RobotNet.Script"]; + UsingNamespacesScript = string.Join(Environment.NewLine, UsingNamespaces.Select(ns => $"using {ns};")); + + List metadataRefs = []; + var currentDirectory = Directory.GetCurrentDirectory(); + if (Directory.Exists(DllsPath)) + { + foreach (var dll in Directory.GetFiles(DllsPath, "*.dll")) + { + metadataRefs.Add(MetadataReference.CreateFromFile(Path.Combine(currentDirectory, dll), properties: MetadataReferenceProperties.Assembly)); + } + } + + MetadataReferences = [.. metadataRefs]; + var options = ScriptOptions.Default; + options.MetadataReferences.Clear(); + ScriptOptions = options.AddReferences(MetadataReferences).AddImports(UsingNamespaces).WithEmitDebugInformation(false); + + DeveloptGlobalsScript = BuildDeveloptGlobalsScript(); + RuntimeGlobalsScript = BuildRuntimeGlobalsScript(); + } + + public static string GetScriptCode() => ReadAllTextFromFolder(ScriptStorePath); + + private static string ReadAllTextFromFolder(string path) + { + var dirInfo = new DirectoryInfo(path); + + var code = string.Join(Environment.NewLine, [.. dirInfo.GetFiles("*.cs").Select(f => File.ReadAllText(f.FullName))]); + var after = string.Join(Environment.NewLine, dirInfo.GetDirectories().Select(dir => ReadAllTextFromFolder(dir.FullName)).ToArray()); + return string.Join(Environment.NewLine, code, after); + } + + private static string BuildRuntimeGlobalsScript() + { + var type = typeof(IRobotNetGlobals); + var sb = new StringBuilder(); + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + sb.AppendLine($@"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name} +{{ + get => ({CSharpSyntaxHelper.ToTypeString(field.FieldType)})robotnet[""{field.Name}""]; + set => robotnet[""{field.Name}""] = value; +}}"); + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.GetIndexParameters().Length == 0) + { + var hasGetter = prop.GetGetMethod() != null; + var hasSetter = prop.GetSetMethod() != null; + if (hasGetter || hasSetter) + { + var propBuilder = new StringBuilder(); + propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} {prop.Name}"); + propBuilder.AppendLine("{"); + if (hasGetter) + propBuilder.AppendLine($@" get => ((Func<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""get_{prop.Name}""])();"); + + if (hasSetter) + propBuilder.AppendLine($@" set => ((Action<{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)}>)robotnet[""set_{prop.Name}""])(value);"); + propBuilder.AppendLine("}"); + sb.AppendLine(propBuilder.ToString()); + } + } + else + { + // Handle indexers (properties with parameters) + var indexParams = prop.GetIndexParameters(); + var paramDecl = string.Join(", ", indexParams.Select(p => $"{CSharpSyntaxHelper.ToTypeString(p.ParameterType)} {p.Name}")); + var paramNames = string.Join(", ", indexParams.Select(p => p.Name)); + var getterDelegateType = $"Func<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>"; + var setterDelegateType = $"Action<{string.Join(", ", indexParams.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(prop.PropertyType)]))}>"; + + var propBuilder = new StringBuilder(); + propBuilder.AppendLine($"{CSharpSyntaxHelper.ToTypeString(prop.PropertyType)} this[{paramDecl}]"); + propBuilder.AppendLine("{"); + if (prop.GetGetMethod() != null) + propBuilder.AppendLine($@" get => (({getterDelegateType})robotnet[""get_{prop.Name}_indexer""])({paramNames});"); + if (prop.GetSetMethod() != null) + propBuilder.AppendLine($@" set => (({setterDelegateType})robotnet[""set_{prop.Name}_indexer""])({(string.IsNullOrEmpty(paramNames) ? "value" : paramNames + ", value")});"); + propBuilder.AppendLine("}"); + sb.AppendLine(propBuilder.ToString()); + } + } + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + if (!method.IsSpecialName) + { + var parameters = method.GetParameters(); + var parametersStr = string.Join(", ", parameters.Select(ToParameterString)); + var args = string.Join(", ", parameters.Select(p => p.Name)); + var returnType = CSharpSyntaxHelper.ToTypeString(method.ReturnType); + + + var paramTypes = string.Join(",", parameters.Select(p => p.ParameterType.FullName)); + var methodKey = $"{method.Name}({paramTypes})"; + + var methodType = method.ReturnType == typeof(void) + ? (parameters.Length == 0 ? "Action" : $"Action<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)))}>") + : $"Func<{string.Join(", ", parameters.Select(p => CSharpSyntaxHelper.ToTypeString(p.ParameterType)).Concat([CSharpSyntaxHelper.ToTypeString(method.ReturnType)]))}>"; + + sb.AppendLine($@"{returnType} {method.Name}({parametersStr}) => (({methodType})robotnet[""{methodKey}""]){(string.IsNullOrEmpty(args) ? "()" : $"({args})")};"); + } + } + + var code = sb.ToString(); + + return sb.ToString(); + } + + private static string BuildDeveloptGlobalsScript() + { + var sb = new StringBuilder(); + var glovalType = typeof(IRobotNetGlobals); + + var properties = glovalType.GetProperties(); + foreach (var property in properties) + { + var propStr = ""; + if (property.CanRead) + { + propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ get; {(property.CanWrite ? "set; " : "")}}}"; + } + else if (property.CanWrite) + { + propStr = $"{CSharpSyntaxHelper.ToTypeString(property.PropertyType)} {property.Name} {{ set => throw new System.NotImplementedException(); }}"; + } + if(string.IsNullOrEmpty(propStr)) continue; + sb.AppendLine(propStr); + } + + var fields = glovalType.GetFields(); + foreach (var field in fields) + { + sb.AppendLine($"{CSharpSyntaxHelper.ToTypeString(field.FieldType)} {field.Name};"); + } + + var methods = glovalType.GetMethods(); + foreach (var method in methods) + { + if (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")) continue; + + var notImplementedMethod = $"{CSharpSyntaxHelper.ToTypeString(method.ReturnType)} {method.Name}({string.Join(',', method.GetParameters().Select(ToParameterString))}) => throw new System.NotImplementedException();"; + sb.AppendLine(notImplementedMethod); + } + + return sb.ToString(); + } + + private static string ToParameterString(ParameterInfo parameter) + { + var modifier = ""; + if (parameter.IsDefined(typeof(ParamArrayAttribute), false)) + modifier = "params "; + else if (parameter.IsIn && parameter.ParameterType.IsByRef && !parameter.IsOut) + modifier = "in "; + else if (parameter.IsOut) + modifier = "out "; + else if (parameter.ParameterType.IsByRef) + modifier = "ref "; + + var typeString = CSharpSyntaxHelper.ToTypeString( + parameter.ParameterType.IsByRef + ? parameter.ParameterType.GetElementType()! + : parameter.ParameterType + ); + + var defaultValue = ""; + if (parameter.HasDefaultValue) + { + if (parameter.DefaultValue != null) + { + if (parameter.ParameterType.IsEnum) + { + defaultValue = $" = {CSharpSyntaxHelper.ToTypeString(parameter.ParameterType)}.{parameter.DefaultValue}"; + } + else if (parameter.DefaultValue is string) + { + defaultValue = $" = \"{parameter.DefaultValue}\""; + } + else if (parameter.DefaultValue is bool b) + { + defaultValue = $" = {b.ToString().ToLower()}"; + } + else + { + defaultValue = $" = {parameter.DefaultValue}"; + } + } + else + { + defaultValue = " = null"; + } + } + + return $"{modifier}{typeString} {parameter.Name}{defaultValue}"; + } + +} diff --git a/RobotNet.ScriptManager/Helpers/VDA5050Helper.cs b/RobotNet.ScriptManager/Helpers/VDA5050Helper.cs new file mode 100644 index 0000000..35042f9 --- /dev/null +++ b/RobotNet.ScriptManager/Helpers/VDA5050Helper.cs @@ -0,0 +1,74 @@ +using RobotNet.RobotShares.Models; +using RobotNet.Script; +using RobotNet.Script.Expressions; + +namespace RobotNet.ScriptManager.Helpers; + +public static class VDA5050ScriptHelper +{ + public static RobotState ConvertToRobotState(RobotStateModel model) + { + bool isReady = model.IsOnline && !model.OrderState.IsProcessing && model.Errors.Length == 0; + if(model.ActionStates.Length > 0) + { + isReady = isReady && model.ActionStates.All(a => !a.IsProcessing); + } + return new RobotState(isReady, model.BatteryState.BatteryVoltage, model.Loads.Length != 0, model.BatteryState.Charging, model.AgvPosition.X, model.AgvPosition.Y, model.AgvPosition.Theta); + } + + public static RobotInstantActionModel ConvertToRobotInstantActionModel(string robotId, RobotAction action) + { + return new RobotInstantActionModel + { + RobotId = robotId, + Action = new RobotShares.VDA5050.InstantAction.Action + { + ActionType = action.ActionType, + BlockingType = action.BlockingType switch + { + BlockingType.NONE => "NONE", + BlockingType.SOFT => "SOFT", + BlockingType.HARD => "HARD", + _ => "NONE" + }, + ActionParameters = [..action.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter + { + Key = p.Key, + Value = p.Value + }) ?? []], + } + }; + } + + public static RobotShares.Models.RobotMoveToNodeModel ConvertToRobotMoveToNodeModel(string robotId, string nodeName, IDictionary> actions, double? lastAngle) + { + return new RobotShares.Models.RobotMoveToNodeModel + { + RobotId = robotId, + NodeName = nodeName, + LastAngle = lastAngle ?? 0, + OverrideLastAngle = lastAngle != null, + Actions = actions.ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.Select(a => new RobotShares.VDA5050.InstantAction.Action + { + ActionType = a.ActionType, + ActionId = Guid.NewGuid().ToString(), + BlockingType = a.BlockingType switch + { + BlockingType.NONE => "NONE", + BlockingType.SOFT => "SOFT", + BlockingType.HARD => "HARD", + _ => "NONE" + }, + ActionParameters = [..a.Parameters?.Select(p => new RobotShares.VDA5050.InstantAction.ActionParameter + { + Key = p.Key, + Value = p.Value + }) ?? []], + }) + ) + }; + } + +} diff --git a/RobotNet.ScriptManager/Hubs/ConsoleHub.cs b/RobotNet.ScriptManager/Hubs/ConsoleHub.cs new file mode 100644 index 0000000..31d7189 --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/ConsoleHub.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class ConsoleHub : Hub +{ + public Task RegisterTasksConsole(string name) => Groups.AddToGroupAsync(Context.ConnectionId, $"task-{name}"); + public Task UnregisterTasksConsole(string name) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"task-{name}"); + public Task RegisterTaskConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "tasks"); + public Task UnregisterTaskConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "tasks"); + public Task RegisterMissionConsole(Guid missionId) => Groups.AddToGroupAsync(Context.ConnectionId, $"mission-{missionId}"); + public Task UnregisterMissionConsole(Guid missionId) => Groups.RemoveFromGroupAsync(Context.ConnectionId, $"mission-{missionId}"); + public Task RegisterMissionConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "missions"); + public Task UnregisterMissionConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "missions"); + public Task RegisterAllConsoles() => Groups.AddToGroupAsync(Context.ConnectionId, "alls"); + public Task UnregisterAllConsoles() => Groups.RemoveFromGroupAsync(Context.ConnectionId, "alls"); + +} diff --git a/RobotNet.ScriptManager/Hubs/DashboardHub.cs b/RobotNet.ScriptManager/Hubs/DashboardHub.cs new file mode 100644 index 0000000..06e9d5e --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/DashboardHub.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares.Dashboard; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class DashboardHub(DashboardPublisher DashboardPublisher, ILogger Logger) : Hub +{ + public MessageResult GetDashboardData() + { + try + { + return new(true, "") { Data = DashboardPublisher.GetData() }; + } + catch (Exception ex) + { + Logger.LogWarning("Lấy dữ liệu Dashboard xảy ra lỗi: {ex}", ex.Message); + return new(false, "Lấy dữ liệu Dashboard xảy ra lỗi"); + } + } +} diff --git a/RobotNet.ScriptManager/Hubs/HMIHub.cs b/RobotNet.ScriptManager/Hubs/HMIHub.cs new file mode 100644 index 0000000..d7e07b0 --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/HMIHub.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class HMIHub(ScriptStateManager scriptBuilder, ScriptGlobalsManager globalsManager) : Hub +{ + public ProcessorState GetState() => scriptBuilder.State; + + public ProcessorRequest GetRequest() => scriptBuilder.Request; + + public IDictionary GetVariables(IEnumerable keys) + { + var variables = new Dictionary(); + foreach (var key in keys) + { + if (globalsManager.Globals.TryGetValue(key, out object? val)) + { + variables.Add(key, val?.ToString() ?? "null"); + } + else + { + variables.Add(key, "null"); + } + } + return variables; + } + + public MessageResult SetVariable(string key, string value) + { + try + { + globalsManager.SetValue(key, value); + return new(true, ""); + } + catch (Exception ex) + { + return new(false, ex.Message); + } + } +} diff --git a/RobotNet.ScriptManager/Hubs/ProcessorHub.cs b/RobotNet.ScriptManager/Hubs/ProcessorHub.cs new file mode 100644 index 0000000..94a64d5 --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/ProcessorHub.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class ProcessorHub(ScriptStateManager scriptBuilder) : Hub +{ + public ProcessorState GetState() => scriptBuilder.State; + + public ProcessorRequest GetRequest() => scriptBuilder.Request; + + public MessageResult Build() + { + var message = ""; + var result = scriptBuilder.Build(ref message); + + return new(result, message); + } + + public MessageResult Run() + { + var message = ""; + var result = scriptBuilder.Run(ref message); + + return new(result, message); + } + + public MessageResult Stop() + { + var message = ""; + var result = scriptBuilder.Stop(ref message); + + return new(result, message); + } + + public MessageResult Reset() + { + var message = ""; + var result = scriptBuilder.Reset(ref message); + + return new(result, message); + } +} diff --git a/RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs b/RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs new file mode 100644 index 0000000..450d18d --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/ScriptOpenHub.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class ScriptOpenHub(ScriptStateManager scriptBuilder, ScriptGlobalsManager globalManager) : Hub +{ + public ProcessorState GetState() => scriptBuilder.State; + + public MessageResult Run() + { + var message = ""; + var result = scriptBuilder.Run(ref message); + + return new(result, message); + } + + public MessageResult Stop() + { + var message = ""; + var result = scriptBuilder.Stop(ref message); + + return new(result, message); + } + + public IDictionary GetVariables() => globalManager.GetVariablesData().ToDictionary(v => v.Name, v => v.Value); + + public string GetVariableValue(string name) + { + if (globalManager.Globals.TryGetValue(name, out var value)) + { + return value?.ToString() ?? "null"; + } + else + { + return "null"; + } + } + + public bool SetVariableValue(string name, string value) + { + try + { + globalManager.SetValue(name, value); + return true; + } + catch + { + return false; + } + } + public bool ResetVariableValue(string name) + { + try + { + globalManager.ResetValue(name); + return true; + } + catch + { + return false; + } + } +} diff --git a/RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs b/RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs new file mode 100644 index 0000000..d75aa01 --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/ScriptTaskHub.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.ScriptManager.Services; +using RobotNet.Shares; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class ScriptTaskHub(ScriptTaskManager taskManager) : Hub +{ + public MessageResult Pause(string name) + { + if (taskManager.Pause(name)) + { + return new MessageResult(true, $"Task '{name}' paused successfully."); + } + return new MessageResult(false, $"Task '{name}' not found or could not be paused."); + } + + public MessageResult Resume(string name) + { + if (taskManager.Resume(name)) + { + return new MessageResult(true, $"Task '{name}' resumed successfully."); + } + return new MessageResult(false, $"Task '{name}' not found or could not be resumed."); + } + + public IDictionary GetTaskStates() + { + return taskManager.GetTaskStates(); + } +} diff --git a/RobotNet.ScriptManager/Hubs/VariablesHub.cs b/RobotNet.ScriptManager/Hubs/VariablesHub.cs new file mode 100644 index 0000000..f980700 --- /dev/null +++ b/RobotNet.ScriptManager/Hubs/VariablesHub.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; +using RobotNet.ScriptManager.Services; + +namespace RobotNet.ScriptManager.Hubs; + +[Authorize] +public class VariablesHub(ScriptGlobalsManager globalsManager) : Hub +{ + public string GetString(string name) + { + if (globalsManager.GetVariableType(name) == typeof(string) && globalsManager.Globals.TryGetValue(name, out var value) && value is string strValue) + { + return strValue; + } + + return string.Empty; + } + + public int GetInt(string name) + { + if (globalsManager.GetVariableType(name) == typeof(int) && globalsManager.Globals.TryGetValue(name, out var value) && value is int intValue) + { + return intValue; + } + return 0; + } + + public bool GetBool(string name) + { + if (globalsManager.GetVariableType(name) == typeof(bool) && globalsManager.Globals.TryGetValue(name, out var value) && value is bool boolValue) + { + return boolValue; + } + return false; + } + + public double GetDouble(string name) + { + if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.TryGetValue(name, out var value) && value is double doubleValue) + { + return doubleValue; + } + return 0.0; + } + + public void SetString(string name, string value) + { + if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.ContainsKey(name)) + { + globalsManager.Globals[name] = value; + } + } + + public void SetInt(string name, int value) + { + if (globalsManager.GetVariableType(name) == typeof(int) && globalsManager.Globals.ContainsKey(name)) + { + globalsManager.Globals[name] = value; + } + } + + public void SetBool(string name, bool value) + { + if (globalsManager.GetVariableType(name) == typeof(bool) && globalsManager.Globals.ContainsKey(name)) + { + globalsManager.Globals[name] = value; + } + } + + public void SetDouble(string name, double value) + { + if (globalsManager.GetVariableType(name) == typeof(double) && globalsManager.Globals.ContainsKey(name)) + { + globalsManager.Globals[name] = value; + } + } +} diff --git a/RobotNet.ScriptManager/Models/ConsoleLog.cs b/RobotNet.ScriptManager/Models/ConsoleLog.cs new file mode 100644 index 0000000..cc7eb7e --- /dev/null +++ b/RobotNet.ScriptManager/Models/ConsoleLog.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.ScriptManager.Hubs; + +namespace RobotNet.ScriptManager.Models; + +public class ConsoleLog(IHubContext consoleHub, ILogger? logger = null) : Script.ILogger +{ + public void LogError(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageError", message)); + logger?.LogError(message); + } + + public void LogInfo(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageInfo", message)); + logger?.LogInformation(message); + } + + public void LogWarning(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.All.SendAsync("MessageWarning", message)); + logger?.LogWarning(message); + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs b/RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs new file mode 100644 index 0000000..bf56594 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptGlobalsRobotNet.cs @@ -0,0 +1,63 @@ +using RobotNet.Script; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Services; + +namespace RobotNet.ScriptManager.Models; + +public abstract class ScriptGlobalsRobotNet(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory) : IRobotNetGlobals +{ + public Script.ILogger Logger { get; } = logger; + + public IRobotManager RobotManager { get; } = robotManager; + + public IMapManager MapManager { get; } = mapManager; + + public virtual Guid CurrentMissionId => throw new NotImplementedException(); + + public IUnixDevice UnixDevice { get; } = Connections.UnixDevice.Instance; + + public IConnectionManager ConnectionManager { get; } = connectionManager; + + public async Task CreateMission(string name, params object[] parameters) + { + using var scope = scopeFactory.CreateScope(); + var missionCreator = scope.ServiceProvider.GetRequiredService(); + return await missionCreator.CreateMissionAsync(name, []); + } + + public async Task CreateMission(string name) => await CreateMission(name, []); + + public bool CancelMission(Guid missionId, string reason) + { + using var scope = scopeFactory.CreateScope(); + var missionManager = scope.ServiceProvider.GetRequiredService(); + return missionManager.Cancel(missionId, reason); + } + + public bool DisableTask(string name) + { + using var scope = scopeFactory.CreateScope(); + var taskManager = scope.ServiceProvider.GetRequiredService(); + return taskManager.Pause(name); + } + + public bool EnableTask(string name) + { + using var scope = scopeFactory.CreateScope(); + var taskManager = scope.ServiceProvider.GetRequiredService(); + return taskManager.Resume(name); + } + +} + +public class ScriptGlobalsRobotNetTask(Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory) + : ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory) +{ + public override Guid CurrentMissionId => throw new NotImplementedException(); +} + +public class ScriptGlobalsRobotNetMission(Guid id, Script.ILogger logger, IRobotManager robotManager, IMapManager mapManager, ScriptConnectionManager connectionManager, IServiceScopeFactory scopeFactory) + : ScriptGlobalsRobotNet(logger, robotManager, mapManager, connectionManager, scopeFactory) +{ + public override Guid CurrentMissionId { get; } = id; +} diff --git a/RobotNet.ScriptManager/Models/ScriptMapElement.cs b/RobotNet.ScriptManager/Models/ScriptMapElement.cs new file mode 100644 index 0000000..4f0c255 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMapElement.cs @@ -0,0 +1,109 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptMapElement(string mapName, + ElementDto element, + Func UpdateIsOpenFunc, + Func? UpdatePropertiesFunc) : Script.IElement +{ + public Guid Id { get; } = element.Id; + + public Guid ModelId { get; } = element.ModelId; + + public string ModelName { get; } = element.ModelName ?? throw new ArgumentNullException(nameof(element), "Model name cannot be null"); + + public Guid NodeId { get; } = element.NodeId; + + public string MapName { get; } = mapName ?? throw new ArgumentNullException(nameof(mapName), "Map name cannot be null"); + + public string Name { get; } = element.Name ?? throw new ArgumentNullException(nameof(element), "Element name cannot be null"); + + public double OffsetX { get; } = element.OffsetX; + + public double OffsetY { get; } = element.OffsetY; + + public string NodeName { get; } = element.NodeName ?? throw new ArgumentNullException(nameof(element), "Node name cannot be null"); + + public double X { get; } = element.X; + + public double Y { get; } = element.Y; + + public double Theta { get; } = element.Theta; + + public Script.Expressions.ElementProperties Properties { get; } = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content); + + private Script.Expressions.ElementProperties StoredProperties = MapManagerExtensions.GetElementProperties(element.IsOpen, element.Content); + + public async Task SaveChangesAsync() + { + if (Properties.IsOpen != StoredProperties.IsOpen && UpdateIsOpenFunc != null) + { + await UpdateIsOpenFunc.Invoke(MapName, Name, Properties.IsOpen); + StoredProperties.IsOpen = Properties.IsOpen; + } + + if (UpdatePropertiesFunc != null) + { + var changedProperties = new List(); + foreach (var prop in StoredProperties.Bool) + { + if (Properties.Bool.TryGetValue(prop.Key, out var value) && value != prop.Value) + { + changedProperties.Add(new MapShares.Property.ElementProperty() + { + Name = prop.Key, + DefaultValue = value.ToString() + }); + } + } + + foreach (var prop in StoredProperties.Double) + { + if (Properties.Double.TryGetValue(prop.Key, out var value) && value != prop.Value) + { + changedProperties.Add(new MapShares.Property.ElementProperty() + { + Name = prop.Key, + DefaultValue = value.ToString() + }); + } + } + + foreach (var prop in StoredProperties.Int) + { + if (Properties.Int.TryGetValue(prop.Key, out var value) && value != prop.Value) + { + changedProperties.Add(new MapShares.Property.ElementProperty() + { + Name = prop.Key, + DefaultValue = value.ToString() + }); + } + } + + foreach (var prop in StoredProperties.String) + { + if (Properties.String.TryGetValue(prop.Key, out var value) && value != prop.Value) + { + changedProperties.Add(new MapShares.Property.ElementProperty() + { + Name = prop.Key, + DefaultValue = value + }); + } + } + + if (changedProperties.Count != 0) + { + var updateModel = new ElementPropertyUpdateModel + { + Properties = [.. changedProperties] + }; + await UpdatePropertiesFunc.Invoke(MapName, Name, updateModel); + StoredProperties = Properties; // Update stored properties after saving + } + } + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptMapManager.cs b/RobotNet.ScriptManager/Models/ScriptMapManager.cs new file mode 100644 index 0000000..63046cb --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMapManager.cs @@ -0,0 +1,319 @@ +using OpenIddict.Client; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Models; +using RobotNet.Script; +using RobotNet.Shares; +using Serialize.Linq.Serializers; +using System.Linq.Expressions; +using System.Net.Http.Headers; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptMapManager(OpenIddictClientService openIddictClient, string MapManagerUrl, string[] MapManagerScopes) : IMapManager +{ + private string? CachedToken; + private DateTime TokenExpiry; + private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer()); + + public Task FindElements(string map, string model) + => FindElements(map, model, element => true); + + public Task FindElements(string map, string model, Expression> expr) + => FindElements(map, model, expr, true); + + private async Task FindElements(string map, string model, Expression> expr, bool retry) + { + var accessToken = await RequestAccessToken(); + using var client = new HttpClient() + { + BaseAddress = new Uri(MapManagerUrl) + }; + + using var request = new HttpRequestMessage(HttpMethod.Post, $"api/ScriptElements"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + // Đưa modelExpression vào body của request + request.Content = JsonContent.Create(new ElementExpressionModel + { + MapName = map, + ModelName = model, + Expression = expressionSerializer.SerializeText(expr), + }); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + CachedToken = null; // Clear cached token to force re-authentication + return await FindElements(map, model, expr, false); // Retry without recall + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync>>(); + if (result == null) + { + throw new InvalidOperationException("Failed to deserialize response from server"); + } + else if (!result.IsSuccess) + { + throw new InvalidOperationException($"Error from server: {result.Message}"); + } + else if (result.Data == null || !result.Data.Any()) + { + return []; + } + + return [.. result.Data.Select(e => new ScriptMapElement(map, e, UpdateIsOpenAsync, UpdatePropertiesAsync))]; + } + } + + public Task GetElement(string map, string name) => GetElement(map, name, true); + + private async Task GetElement(string map, string name, bool retry) + { + if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Map name and element name cannot be null or empty"); + } + + var accessToken = await RequestAccessToken(); + + using var client = new HttpClient() + { + BaseAddress = new Uri(MapManagerUrl) + }; + using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/element/{name}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + CachedToken = null; // Clear cached token to force re-authentication + return await GetElement(map, name, false); // Retry without recall + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync>(); + if (result == null) + { + throw new InvalidOperationException("Failed to deserialize response from server"); + } + else if (!result.IsSuccess) + { + throw new InvalidOperationException($"Error from server: {result.Message}"); + } + else if (result.Data == null) + { + throw new KeyNotFoundException("Element not found in response"); + } + else + { + return new ScriptMapElement(map, result.Data, UpdateIsOpenAsync, UpdatePropertiesAsync); + } + } + } + + private Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen) => UpdateIsOpenAsync(mapName, elementName, isOpen, true); + + private async Task UpdateIsOpenAsync(string mapName, string elementName, bool isOpen, bool retry) + { + var accessToken = await RequestAccessToken(); + using var client = new HttpClient() + { + BaseAddress = new Uri(MapManagerUrl) + }; + + using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}/IsOpen?isOpen={isOpen}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + //request.Content = JsonContent.Create(new { isOpen }); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + CachedToken = null; // Clear cached token to force re-authentication + await UpdateIsOpenAsync(mapName, elementName, isOpen, false); // Retry without recall + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to update IsOpen: {status.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync(); + if (result == null) + { + throw new InvalidOperationException("Failed to deserialize response from server"); + } + else if (!result.IsSuccess) + { + throw new InvalidOperationException($"Error from server: {result.Message}"); + } + } + } + private Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model) + => UpdatePropertiesAsync(mapName, elementName, model, true); + + private async Task UpdatePropertiesAsync(string mapName, string elementName, ElementPropertyUpdateModel model, bool retry) + { + var accessToken = await RequestAccessToken(); + using var client = new HttpClient() + { + BaseAddress = new Uri(MapManagerUrl) + }; + + using var request = new HttpRequestMessage(HttpMethod.Patch, $"api/ScriptElements/{mapName}/element/{elementName}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + // Đưa modelExpression vào body của request + request.Content = JsonContent.Create(model); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + CachedToken = null; // Clear cached token to force re-authentication + await UpdatePropertiesAsync(mapName, elementName, model, false); // Retry without recall + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to get elements: {status.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync(); + if (result == null) + { + throw new InvalidOperationException("Failed to deserialize response from server"); + } + else if (!result.IsSuccess) + { + throw new InvalidOperationException($"Error from server: {result.Message}"); + } + } + } + + public async Task RequestAccessToken() + { + try + { + if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry) + { + return CachedToken; + } + + var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new() + { + Scopes = [.. MapManagerScopes], + }); + + if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null) + { + return null; + } + else + { + TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime; + CachedToken = result.AccessToken; + return CachedToken; + } + } + catch + { + return null; + } + } + + public Task GetNode(string map, string name) => GetNode(map, name, true); + + private async Task GetNode(string map, string name, bool retry) + { + if (string.IsNullOrEmpty(map) || string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Map name and element name cannot be null or empty"); + } + + var accessToken = await RequestAccessToken(); + + using var client = new HttpClient() + { + BaseAddress = new Uri(MapManagerUrl) + }; + using var request = new HttpRequestMessage(HttpMethod.Get, $"api/ScriptElements/{map}/node/{name}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + CachedToken = null; // Clear cached token to force re-authentication + return await GetNode(map, name, false); // Retry without recall + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired. Please re-authenticate."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to get element: {status.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync>(); + if (result == null) + { + throw new InvalidOperationException("Failed to deserialize response from server"); + } + else if (!result.IsSuccess) + { + throw new InvalidOperationException($"Error from server: {result.Message}"); + } + else if (result.Data == null) + { + throw new KeyNotFoundException("Element not found in response"); + } + else + { + return new ScriptMapNode(result.Data.Id, result.Data.MapId, result.Data.Name, result.Data.X, result.Data.Y, result.Data.Theta); + } + } + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptMapNode.cs b/RobotNet.ScriptManager/Models/ScriptMapNode.cs new file mode 100644 index 0000000..1eee950 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMapNode.cs @@ -0,0 +1,3 @@ +namespace RobotNet.ScriptManager.Models; + +public record ScriptMapNode(Guid Id, Guid MapId, string Name, double X, double Y, double Theta) : Script.INode; diff --git a/RobotNet.ScriptManager/Models/ScriptMission.cs b/RobotNet.ScriptManager/Models/ScriptMission.cs new file mode 100644 index 0000000..4e458fe --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMission.cs @@ -0,0 +1,298 @@ +using Microsoft.CodeAnalysis.Scripting; +using RobotNet.Script; +using RobotNet.Script.Shares; +using System.Collections.Concurrent; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptMissionGlobals(IDictionary _globals, IDictionary _robotnet, ConcurrentDictionary _parameters) +{ + public readonly IDictionary globals = _globals; + public readonly IDictionary robotnet = _robotnet; + public readonly IDictionary parameters = _parameters; +} +public class ScriptMission : IDisposable +{ + public Guid Id { get; } + public string Name { get; } + public MissionStatus Status { get; private set; } = MissionStatus.Idle; + public Exception? Exception { get; private set; } = null; + public CancellationToken CancellationToken => internalCts?.Token ?? CancellationToken.None; + public WaitHandle WaitHandle => waitHandle; + + private readonly ScriptRunner> Runner; + private readonly ScriptMissionGlobals Globals; + private readonly CancellationTokenSource internalCts; + private readonly Thread thread; + private bool disposed; + private readonly Mutex mutex = new(); + private readonly ScriptMissionLogger? Logger; + private readonly ManualResetEvent waitHandle = new(false); + + public ScriptMission(Guid id, string name, ScriptRunner> runner, ScriptMissionGlobals globals, CancellationTokenSource cts) + { + Id = id; + Name = name; + Runner = runner; + Globals = globals; + internalCts = cts; + thread = new Thread(() => Run(internalCts.Token)) { IsBackground = true, Priority = ThreadPriority.Highest }; + + if (Globals.robotnet.TryGetValue($"get_{nameof(IRobotNetGlobals.Logger)}", out object? get_logger) + && get_logger is Func get_logger_func) + { + Logger = get_logger_func.Invoke() as ScriptMissionLogger; + } + } + + public string GetLog() => Logger?.GetLog() ?? string.Empty; + + public void Start() + { + if (!mutex.WaitOne(1000)) return; + + try + { + ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); + if (Status != MissionStatus.Idle) throw new InvalidOperationException("Mission can only be started after preparation."); + if (internalCts.IsCancellationRequested) throw new InvalidOperationException("Mission is not prepared or has been canceled."); + + Status = MissionStatus.Running; + thread.Start(); + } + finally + { + mutex.ReleaseMutex(); + } + } + + public bool Pause() + { + if (!mutex.WaitOne(1000)) return false; + + try + { + ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); + if (Status == MissionStatus.Running) + { + Status = MissionStatus.Pausing; + return true; + } + else + { + return false; // Cannot pause if not running + } + } + catch (Exception) + { + return false; + } + finally + { + mutex.ReleaseMutex(); + } + } + + public bool Resume() + { + if (!mutex.WaitOne(1000)) return false; + try + { + ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); + if (Status == MissionStatus.Paused) + { + Status = MissionStatus.Resuming; + return true; + } + else + { + return false; // Cannot resume if not paused + } + } + catch (Exception) + { + return false; + } + finally + { + mutex.ReleaseMutex(); + } + } + + public bool Cancel(string reason) + { + if (!mutex.WaitOne(1000)) + { + return false; + } + try + { + ObjectDisposedException.ThrowIf(disposed, nameof(ScriptMission)); + if (!internalCts.IsCancellationRequested) + { + internalCts.Cancel(); + } + if (Status == MissionStatus.Canceling || Status == MissionStatus.Canceled) + { + return true; + } + + if (Status == MissionStatus.Idle) + { + Logger?.LogInfo($"{DateTime.UtcNow} Idle Mission '{Name}' cancel with reason: {reason}"); + Status = MissionStatus.Canceled; + return true; + } + else if (Status == MissionStatus.Running || Status == MissionStatus.Pausing || Status == MissionStatus.Paused || Status == MissionStatus.Resuming) + { + Status = MissionStatus.Canceling; + Logger?.LogInfo($"{DateTime.UtcNow} Mission '{Name}' canceling with reason: {reason}"); + return true; + } + else + { + return true; + } + } + catch (Exception) + { + return false; + } + finally + { + mutex.ReleaseMutex(); + } + } + + private void Run(CancellationToken cancellationToken) + { + Task.Factory.StartNew(async Task () => + { + try + { + var cts = new CancellationTokenSource(); + cancellationToken.Register(() => + { + cts.CancelAfter(TimeSpan.FromSeconds(5)); + }); + await foreach (var state in await Runner.Invoke(Globals, cts.Token)) + { + Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} with step: {state.Step}; message: {state.Message}"); + if (Status == MissionStatus.Pausing) + { + Status = MissionStatus.Paused; + while (Status == MissionStatus.Paused && !cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(1000, cancellationToken); + } + catch (TaskCanceledException) { } + catch (OperationCanceledException) { } + } + } + + if (cancellationToken.IsCancellationRequested) + { + if (Status != MissionStatus.Canceling) + { + Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally."); + Logger?.LogError(Exception); + Status = MissionStatus.Error; + } + break; + } + + if (Status == MissionStatus.Resuming) + { + Status = MissionStatus.Running; + continue; + } + else if (Status == MissionStatus.Canceling) + { + if (internalCts == null || internalCts.IsCancellationRequested) + { + Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled externally."); + Logger?.LogError(Exception); + Status = MissionStatus.Error; + break; + } + else + { + Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceling"); + continue; + } + } + else if (Status == MissionStatus.Running) + { + continue; + } + else if (Status != MissionStatus.Error) + { + Status = MissionStatus.Error; + Exception = new InvalidOperationException($"{DateTime.UtcNow} Mission {Name}-{Id} Unexpected status change: {Status}"); + Logger?.LogError(Exception); + break; + } + } + + if (Status == MissionStatus.Running) + { + Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} is completed"); + Status = MissionStatus.Completed; + } + else if (Status == MissionStatus.Canceling) + { + Logger?.LogError($"{DateTime.UtcNow} Mission {Name}-{Id} is canceled"); + Status = MissionStatus.Canceled; + } + else if (Status != MissionStatus.Error) + { + Exception = new OperationCanceledException($"{DateTime.UtcNow} Mission {Name}-{Id} Cancel from status {Status}"); + Logger?.LogError(Exception); + Status = MissionStatus.Error; + } + } + catch (OperationCanceledException oce) + { + if (Status != MissionStatus.Canceling) + { + Status = MissionStatus.Error; + Exception = oce; + Logger?.LogError(oce.ToString()); + } + else + { + Logger?.LogInfo($"{DateTime.UtcNow} Mission {Name}-{Id} was canceled successfully."); + Status = MissionStatus.Canceled; + } + } + catch (Exception ex) + { + Status = MissionStatus.Error; + Exception = ex; + Logger?.LogError(Exception); + } + finally + { + waitHandle.Set(); + Logger?.LogInfo($"{DateTime.UtcNow} End Mission wtih status: {Status}"); + } + }, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current).GetAwaiter().GetResult(); + } + + public void Dispose() + { + if (disposed) return; + disposed = true; + + if (!internalCts.IsCancellationRequested) + { + internalCts.Cancel(); + } + thread.Join(); + internalCts.Dispose(); + GC.SuppressFinalize(this); + } + +} diff --git a/RobotNet.ScriptManager/Models/ScriptMissionData.cs b/RobotNet.ScriptManager/Models/ScriptMissionData.cs new file mode 100644 index 0000000..15d0ae4 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMissionData.cs @@ -0,0 +1,4 @@ +namespace RobotNet.ScriptManager.Models; + +public record ScriptMissionParameter(string Name, Type Type, object? DefaultValue = null); +public record ScriptMissionData(string Name, IEnumerable Parameters, string Code, string Script, bool IsMultipleRun); diff --git a/RobotNet.ScriptManager/Models/ScriptMissionLogger.cs b/RobotNet.ScriptManager/Models/ScriptMissionLogger.cs new file mode 100644 index 0000000..e94a764 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptMissionLogger.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.ScriptManager.Hubs; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptMissionLogger(IHubContext consoleHub, Guid missionId) : Script.ILogger +{ + private string log = ""; + private readonly Mutex mutexLog = new(); + public void LogError(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageError", message)); + + mutexLog.WaitOne(); + log += $"E {DateTime.UtcNow} {message}{Environment.NewLine}"; + mutexLog.ReleaseMutex(); + } + + public void LogError(Exception ex) => LogError($"{ex.GetType().FullName} {ex.Message}"); + + public void LogInfo(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageInfo", message)); + + mutexLog.WaitOne(); + log += $"I {DateTime.UtcNow} {message}{Environment.NewLine}"; + mutexLog.ReleaseMutex(); + } + + public void LogWarning(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "missions", $"mission-{missionId}").SendAsync("MessageWarning", message)); + + mutexLog.WaitOne(); + log += $"W {DateTime.UtcNow} {message}{Environment.NewLine}"; + mutexLog.ReleaseMutex(); + } + + public string GetLog() + { + mutexLog.WaitOne(); + var result = log; + log = ""; // Clear log after reading + mutexLog.ReleaseMutex(); + return result; + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptRobotManager.cs b/RobotNet.ScriptManager/Models/ScriptRobotManager.cs new file mode 100644 index 0000000..754acdd --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptRobotManager.cs @@ -0,0 +1,185 @@ +using OpenIddict.Client; +using RobotNet.MapShares.Models; +using RobotNet.RobotShares.Models; +using RobotNet.Script; +using RobotNet.Script.Expressions; +using RobotNet.ScriptManager.Clients; +using RobotNet.ScriptManager.Helpers; +using RobotNet.Shares; +using Serialize.Linq.Serializers; +using System.Linq.Expressions; +using System.Net.Http.Headers; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptRobotManager(OpenIddictClientService openIddictClient, string RobotManagerUrl, string[] RobotManagerScopes) : IRobotManager +{ + private string? CachedToken; + private DateTime TokenExpiry; + private static readonly ExpressionSerializer expressionSerializer = new(new Serialize.Linq.Serializers.JsonSerializer()); + + public async Task GetRobotById(string robotId) + { + var robot = new RobotManagerHubClient(robotId, $"{RobotManagerUrl}/hubs/robot-manager", async () => + { + var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new() + { + Scopes = [.. RobotManagerScopes], + }); + if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null) + { + return null; + } + else + { + return result.AccessToken; + } + }); + await robot.StartAsync(); + return robot; + } + + public Task GetRobotState(string robotId) => GetRobotState(robotId, true); + public async Task GetRobotState(string robotId, bool retry) + { + var accessToken = await RequestAccessToken(); + if (string.IsNullOrEmpty(accessToken)) + { + throw new ArgumentException("Failed to get access token"); + } + + using var client = new HttpClient() + { + BaseAddress = new Uri(RobotManagerUrl) + }; + using var request = new HttpRequestMessage(HttpMethod.Get, $"api/RobotManager/State/{robotId}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + // Retry with a new access token + CachedToken = null; // Clear cached token to force a new request + return await GetRobotState(robotId, false); + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired."); + } + } + else if (status.StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to get robot state: {status.ReasonPhrase}"); + } + else + { + var state = await response.Content.ReadFromJsonAsync>(); + if (state == null) + { + throw new InvalidOperationException("Failed to deserialize robot state response"); + } + else if (!state.IsSuccess) + { + throw new InvalidOperationException($"Robot Manager error: {state.Message}"); + } + else + { + if (state.Data == null) + { + throw new InvalidOperationException("Robot state data is null."); + } + return VDA5050ScriptHelper.ConvertToRobotState(state.Data); + } + } + } + + public Task> SearchRobots(string map, string model) + => SearchRobots(map, model, robot => true, true); + + public Task> SearchRobots(string map, string model, Expression> expr) + => SearchRobots(map, model, expr, true); + + public async Task> SearchRobots(string map, string model, Expression> expr, bool retry) + { + var accessToken = await RequestAccessToken(); + using var client = new HttpClient() + { + BaseAddress = new Uri(RobotManagerUrl) + }; + using var request = new HttpRequestMessage(HttpMethod.Post, $"api/RobotManager/Search"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + request.Content = JsonContent.Create(new ElementExpressionModel + { + MapName = map, + ModelName = model, + Expression = expressionSerializer.SerializeText(expr), + }); + + using var response = await client.SendAsync(request); + using var status = response.EnsureSuccessStatusCode(); + if (status.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + if (retry) + { + // Retry with a new access token + CachedToken = null; // Clear cached token to force a new request + return await SearchRobots(map, model, expr, false); + } + else + { + throw new UnauthorizedAccessException("Access token is invalid or expired."); + } + } + else if (response.EnsureSuccessStatusCode().StatusCode != System.Net.HttpStatusCode.OK) + { + throw new HttpRequestException($"Failed to search robots: {response.ReasonPhrase}"); + } + else + { + var result = await response.Content.ReadFromJsonAsync>() ?? throw new Exception("Failed to convert result from Robot Manager"); + if (result.IsSuccess) + { + return result.Data ?? []; + } + else + { + throw new Exception($"Robot Manager error: {result.Message}"); + } + } + } + + public async Task RequestAccessToken() + { + try + { + if (!string.IsNullOrEmpty(CachedToken) && DateTime.UtcNow < TokenExpiry) + { + return CachedToken; + } + + var result = await openIddictClient.AuthenticateWithClientCredentialsAsync(new() + { + Scopes = [.. RobotManagerScopes], + }); + + if (result == null || result.AccessToken == null || result.AccessTokenExpirationDate == null) + { + return null; + } + else + { + TokenExpiry = result.AccessTokenExpirationDate.Value.UtcDateTime; + CachedToken = result.AccessToken; + return CachedToken; + } + } + catch + { + return null; + } + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptTask.cs b/RobotNet.ScriptManager/Models/ScriptTask.cs new file mode 100644 index 0000000..cccb4d0 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptTask.cs @@ -0,0 +1,155 @@ +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using RobotNet.Script.Shares; +using System.Diagnostics; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptTask : IDisposable +{ + public bool Paused { get; private set; } = false; + private readonly ScriptRunner scriptRunner; + private readonly ScriptTaskGlobals globals; + private readonly int Interval; + private readonly double ProcessTime; + private CancellationTokenSource? internalCts; + private Thread? thread; + private bool disposed; + private readonly string Name; + private readonly RobotNet.Script.ILogger? Logger; + private readonly bool AutoStart; + + public class ScriptTaskGlobals(IDictionary _globals, IDictionary _robotnet) + { + public IDictionary globals = _globals; + public IDictionary robotnet = _robotnet; + } + + public ScriptTask(ScriptTaskData task, ScriptOptions options, IDictionary _globals, IDictionary _robotnet) + { + var script = CSharpScript.Create(task.Script, options, globalsType: typeof(ScriptTaskGlobals)); + scriptRunner = script.CreateDelegate(); + + Name = task.Name; + globals = new ScriptTaskGlobals(_globals, _robotnet); + Interval = task.Interval; + ProcessTime = Interval * 0.8; + AutoStart = task.AutoStart; + Paused = !task.AutoStart; + + if (globals.robotnet.TryGetValue($"get_{nameof(IRobotNetGlobals.Logger)}", out object? get_logger) + && get_logger is Func get_logger_func) + { + Logger = get_logger_func.Invoke(); + } + } + + public void Start(CancellationToken cancellationToken = default) + { + Stop(); // Ensure previous thread is stopped before starting a new one + + internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + Paused = !AutoStart; + thread = new Thread(RunningHandler) + { + IsBackground = true, + Priority = ThreadPriority.Highest, + }; + thread.Start(); + } + + public void Pause() + { + Paused = true; + } + + public void Resume() + { + Paused = false; + } + + private void RunningHandler() + { + if (internalCts == null || internalCts.IsCancellationRequested) + { + return; + } + var cts = new CancellationTokenSource(); + var stopwatch = Stopwatch.StartNew(); + int elapsed; + int remaining; + + internalCts.Token.Register(() => + { + cts.CancelAfter(TimeSpan.FromMilliseconds(Interval * 3)); + }); + + try + { + while (!internalCts.IsCancellationRequested) + { + if (Paused) + { + Thread.Sleep(Interval); // Sleep briefly to avoid busy waiting + continue; + } + stopwatch.Restart(); + scriptRunner.Invoke(globals, cts.Token).GetAwaiter().GetResult(); + + stopwatch.Stop(); + elapsed = (int)stopwatch.ElapsedMilliseconds; + remaining = Interval - elapsed; + + // If execution time exceeds ProcessTime, add another cycle + if (elapsed > ProcessTime) + { + remaining += Interval; + } + + if (remaining > 0) + { + try + { + Thread.Sleep(remaining); + } + catch (ThreadInterruptedException) + { + break; + } + } + GC.Collect(); + } + } + catch (Exception ex) + { + Logger?.LogError($"Task \"{Name}\" execution failed: {ex}"); + } + } + + public void Stop() + { + if (internalCts != null && !internalCts.IsCancellationRequested) + { + internalCts.Cancel(); + } + + if (thread != null && thread.IsAlive) + { + //thread.Interrupt(); + thread.Join(); + } + + internalCts?.Dispose(); + internalCts = null; + thread = null; + } + + public void Dispose() + { + if (disposed) return; + Stop(); + disposed = true; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptTaskData.cs b/RobotNet.ScriptManager/Models/ScriptTaskData.cs new file mode 100644 index 0000000..056b804 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptTaskData.cs @@ -0,0 +1,3 @@ +namespace RobotNet.ScriptManager.Models; + +public record ScriptTaskData(string Name, int Interval, bool AutoStart, string Code, string Script); diff --git a/RobotNet.ScriptManager/Models/ScriptTaskLogger.cs b/RobotNet.ScriptManager/Models/ScriptTaskLogger.cs new file mode 100644 index 0000000..bb06dc2 --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptTaskLogger.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.ScriptManager.Hubs; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptTaskLogger(IHubContext consoleHub, string name) : Script.ILogger +{ + public void LogError(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageError", message)); + } + + public void LogInfo(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageInfo", message)); + } + + public void LogWarning(string message) + { + _ = Task.Factory.StartNew(Task () => consoleHub.Clients.Groups("alls", "tasks", $"task-{name}").SendAsync("MessageWarning", message)); + } +} diff --git a/RobotNet.ScriptManager/Models/ScriptVariable.cs b/RobotNet.ScriptManager/Models/ScriptVariable.cs new file mode 100644 index 0000000..0a2313b --- /dev/null +++ b/RobotNet.ScriptManager/Models/ScriptVariable.cs @@ -0,0 +1,23 @@ +using RobotNet.ScriptManager.Helpers; + +namespace RobotNet.ScriptManager.Models; + +public class ScriptVariableSyntax(string name, Type type, object? defaultValue, bool publicRead, bool publicWrite) +{ + public string Name { get; } = name; + public Type Type { get; } = type; + public string TypeName { get; } = CSharpSyntaxHelper.ToTypeString(type); + public object? DefaultValue { get; } = defaultValue; + public bool PublicRead { get; } = publicRead; + public bool PublicWrite { get; } = publicWrite; + + public override bool Equals(object? obj) + { + if (obj == null) return false; + if(obj is not ScriptVariableSyntax variable) return false; + if (variable.Name != Name) return false; + return true; + } + + public override int GetHashCode() => Name.GetHashCode(); +} diff --git a/RobotNet.ScriptManager/Program.cs b/RobotNet.ScriptManager/Program.cs new file mode 100644 index 0000000..10d2409 --- /dev/null +++ b/RobotNet.ScriptManager/Program.cs @@ -0,0 +1,139 @@ +using Microsoft.EntityFrameworkCore; +using NLog.Web; +using OpenIddict.Client; +using OpenIddict.Validation.AspNetCore; +using RobotNet.OpenIddictClient; +using RobotNet.ScriptManager.Data; +using RobotNet.ScriptManager.Services; +using static OpenIddict.Abstractions.OpenIddictConstants; + +var builder = WebApplication.CreateBuilder(args); + +//builder.Logging.ClearProviders(); +builder.Host.UseNLog(); + +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + +var openIddictOption = builder.Configuration.GetSection(nameof(OpenIddictClientProviderOptions)).Get() + ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + +builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); + +builder.Services.AddControllers(); +builder.Services.AddSignalR(); + +builder.Services.AddOpenIddict() + .AddValidation(options => + { + // Note: the validation handler uses OpenID Connect discovery + // to retrieve the address of the introspection endpoint. + options.SetIssuer(openIddictOption.Issuer); + options.AddAudiences(openIddictOption.Audiences); + + // Configure the validation handler to use introspection and register the client + // credentials used when communicating with the remote introspection endpoint. + options.UseIntrospection() + .SetClientId(openIddictOption.ClientId) + .SetClientSecret(openIddictOption.ClientSecret); + + // Register the System.Net.Http integration. + if (builder.Environment.IsDevelopment()) + { + options.UseSystemNetHttp(httpOptions => + { + httpOptions.ConfigureHttpClientHandler(context => + { + context.ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true; + }); + }); + } + else + { + options.UseSystemNetHttp(); + } + + // Register the ASP.NET Core host. + options.UseAspNetCore(); + }) + .AddClient(options => + { + // Allow grant_type=client_credentials to be negotiated. + options.AllowClientCredentialsFlow(); + + // Disable token storage, which is not necessary for non-interactive flows like + // grant_type=password, grant_type=client_credentials or grant_type=refresh_token. + options.DisableTokenStorage(); + + // Register the System.Net.Http integration and use the identity of the current + // assembly as a more specific user agent, which can be useful when dealing with + // providers that use the user agent as a way to throttle requests (e.g Reddit). + options.UseSystemNetHttp() + .SetProductInformation(typeof(Program).Assembly); + + var registration = new OpenIddictClientRegistration + { + Issuer = new Uri(openIddictOption.Issuer, UriKind.Absolute), + GrantTypes = { GrantTypes.ClientCredentials }, + ClientId = openIddictOption.ClientId, + ClientSecret = openIddictOption.ClientSecret, + }; + + foreach (var scope in openIddictOption.Scopes) + { + registration.Scopes.Add(scope); + } + + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(registration); + }); + +builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); +builder.Services.AddAuthorization(); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding"); + }); +}); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); + +var app = builder.Build(); +await app.Services.SeedScriptManagerDbAsync(); +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseCors(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapStaticAssets(); +app.MapControllers(); + +app.MapHub("/hubs/console"); +app.MapHub("/hubs/processor"); +app.MapHub("/hubs/scripttask"); +app.MapHub("/hubs/dashboard"); +app.MapHub("/hubs/script-open"); +app.MapHub("/hubs/hmi"); + +app.Run(); diff --git a/RobotNet.ScriptManager/Properties/launchSettings.json b/RobotNet.ScriptManager/Properties/launchSettings.json new file mode 100644 index 0000000..ee8fe23 --- /dev/null +++ b/RobotNet.ScriptManager/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "workingDirectory": "$(TargetDir)", + "applicationUrl": "https://localhost:7102", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RobotNet.ScriptManager/RobotNet.ScriptManager.csproj b/RobotNet.ScriptManager/RobotNet.ScriptManager.csproj new file mode 100644 index 0000000..7e14e0f --- /dev/null +++ b/RobotNet.ScriptManager/RobotNet.ScriptManager.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RobotNet.ScriptManager/Services/DashboardConfig.cs b/RobotNet.ScriptManager/Services/DashboardConfig.cs new file mode 100644 index 0000000..26a6cf7 --- /dev/null +++ b/RobotNet.ScriptManager/Services/DashboardConfig.cs @@ -0,0 +1,36 @@ +using System.Text.Json; + +namespace RobotNet.ScriptManager.Services; + +public class DashboardConfig : BackgroundService +{ + public string[] MissionNames => [.. Config.MissionNames ?? []]; + private ConfigData Config; + private const string DataPath = "dashboardConfig.json"; + private struct ConfigData + { + public List MissionNames { get; set; } + } + + public async Task UpdateMissionNames(string[] names) + { + Config.MissionNames = [..names]; + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (File.Exists(DataPath)) + { + try + { + Config = JsonSerializer.Deserialize(await File.ReadAllTextAsync(DataPath, CancellationToken.None)); + } + catch (JsonException) + { + await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None); + } + } + else await File.WriteAllTextAsync(DataPath, JsonSerializer.Serialize(Config), CancellationToken.None); + } +} diff --git a/RobotNet.ScriptManager/Services/DashboardPublisher.cs b/RobotNet.ScriptManager/Services/DashboardPublisher.cs new file mode 100644 index 0000000..7dcc8d4 --- /dev/null +++ b/RobotNet.ScriptManager/Services/DashboardPublisher.cs @@ -0,0 +1,115 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares.Dashboard; +using RobotNet.ScriptManager.Data; +using RobotNet.ScriptManager.Hubs; +using System; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace RobotNet.ScriptManager.Services; + +public class DashboardPublisher(DashboardConfig Config, IServiceProvider ServiceProvider, IConfiguration Configuration, IHubContext DashboardHub, ILogger Logger) : BackgroundService +{ + private readonly int UpdateTime = Configuration.GetValue("Dashboard:UpdateTimeMilliSeconds", 5000); + private readonly int CycleDate = Configuration.GetValue("Dashboard:CycleDate", 7); + + private static TaktTimeMissionDto GetTaktTime(InstanceMission[] missions, DateTime date) + { + TaktTimeMissionDto taktTime = new() { Label = date.ToString("dd/MM/yyyy") }; + if (missions.Length > 0) + { + double TaktTimeAll = 0; + foreach (var mission in missions) + { + var time = mission.StopedAt - mission.CreatedAt; + if (time.TotalMinutes > 0) + { + TaktTimeAll += time.TotalMinutes; + if (time.TotalMinutes < taktTime.Min || taktTime.Min == 0) taktTime.Min = time.TotalMinutes; + if (time.TotalMinutes > taktTime.Max || taktTime.Max == 0) taktTime.Max = time.TotalMinutes; + } + } + taktTime.Average = TaktTimeAll / missions.Length; + } + return taktTime; + } + + private bool IsMissionInConfig(string missionName) + { + return Config.MissionNames.Length == 0 || Config.MissionNames.Contains(missionName); + } + + public DashboardDto GetData() + { + using var scope = ServiceProvider.CreateScope(); + var MissionDb = scope.ServiceProvider.GetRequiredService(); + List TotalMissionPerformance = []; + List TaktTimeMissions = []; + var startDate = DateTime.Today.Date.AddDays(-CycleDate); + for (var i = startDate; i <= DateTime.Today.Date; i = i.AddDays(1)) + { + var missions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == i.Date).ToList(); + missions = [.. missions.Where(im => IsMissionInConfig(im.MissionName))]; + var completedMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList(); + var errorMissions = missions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList(); + TotalMissionPerformance.Add(new DailyPerformanceDto + { + Label = i.ToString("dd/MM/yyyy"), + Completed = completedMissions.Count, + Error = errorMissions.Count, + Other = missions.Count - completedMissions.Count - errorMissions.Count, + }); + TaktTimeMissions.Add(GetTaktTime([.. completedMissions], i)); + } + DailyPerformanceDto TodayPerformance = TotalMissionPerformance[^1]; + DailyPerformanceDto ThisWeekPerformance = new() + { + Completed = TotalMissionPerformance.Sum(dp => dp.Completed), + Error = TotalMissionPerformance.Sum(dp => dp.Error), + Other = TotalMissionPerformance.Sum(dp => dp.Other), + }; + + var toDayMissions = MissionDb.InstanceMissions.Where(im => im.CreatedAt.Date == DateTime.Today.Date).ToList(); + toDayMissions = [.. toDayMissions.Where(im => IsMissionInConfig(im.MissionName))]; + var toDaycompletedMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Completed).ToList(); + var toDayerrorMissions = toDayMissions.Where(im => im.Status == Script.Shares.MissionStatus.Error).ToList(); + var toDayTaktTime = GetTaktTime([.. toDaycompletedMissions], DateTime.Today); + + DailyMissionDto DailyMission = new() + { + Completed = toDaycompletedMissions.Count, + Error = toDayerrorMissions.Count, + Total = toDayMissions.Count, + CompletedRate = toDayMissions.Count > 0 ? (int)((toDaycompletedMissions.Count * 100.0 / toDayMissions.Count) + 0.5) : 0, + TaktTimeMin = toDayTaktTime.Min, + TaktTimeAverage = toDayTaktTime.Average, + TaktTimeMax = toDayTaktTime.Max, + RobotOnline = 1, + }; + + + return new() + { + TaktTimeMissions = [.. TaktTimeMissions], + ThisWeekPerformance = ThisWeekPerformance, + TodayPerformance = TodayPerformance, + TotalMissionPerformance = [.. TotalMissionPerformance], + DailyMission = DailyMission, + }; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Yield(); + while (!stoppingToken.IsCancellationRequested) + { + try + { + await Task.WhenAll(DashboardHub.Clients.All.SendAsync("DashboardDataUpdated", GetData(), cancellationToken: stoppingToken), Task.Delay(UpdateTime, stoppingToken)); + } + catch (Exception ex) + { + Logger.LogWarning("Dashboard Publisher xảy ra lỗi: {ex}", ex.Message); + } + } + } +} diff --git a/RobotNet.ScriptManager/Services/LoopService.cs b/RobotNet.ScriptManager/Services/LoopService.cs new file mode 100644 index 0000000..1e6c1d4 --- /dev/null +++ b/RobotNet.ScriptManager/Services/LoopService.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +namespace RobotNet.ScriptManager.Services; + +public abstract class LoopService(int interval) : IHostedService +{ + private readonly TimeSpan Interval = TimeSpan.FromMilliseconds(interval); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly Stopwatch stopwatch = new(); + private Timer? timer; + + public async Task StartAsync(CancellationToken cancellationToken) + { + await BeforExecuteAsync(cancellationToken); + stopwatch.Start(); + timer = new Timer(Callback, cancellationTokenSource, 0, Timeout.Infinite); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + cancellationTokenSource.Cancel(); + timer?.Dispose(); + await AfterExecuteAsync(cancellationToken); + } + protected virtual Task BeforExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask; + protected abstract void Execute(CancellationToken stoppingToken); + protected virtual Task AfterExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + private void Callback(object? state) + { + if (state is not CancellationTokenSource cts || cts.IsCancellationRequested) return; + + Execute(cts.Token); + + if (!cts.IsCancellationRequested) + { + long nextTrigger = Interval.Ticks - (stopwatch.ElapsedTicks % Interval.Ticks); + if (nextTrigger <= 0) nextTrigger = Interval.Ticks; + + int nextIntervalMs = (int)(nextTrigger / TimeSpan.TicksPerMillisecond); + + timer?.Change(nextIntervalMs, Timeout.Infinite); + } + } +} diff --git a/RobotNet.ScriptManager/Services/ScriptConnectionManager.cs b/RobotNet.ScriptManager/Services/ScriptConnectionManager.cs new file mode 100644 index 0000000..6508741 --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptConnectionManager.cs @@ -0,0 +1,80 @@ +using RobotNet.Script; +using RobotNet.ScriptManager.Connections; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptConnectionManager : IConnectionManager +{ + private readonly SemaphoreSlim _connectLock = new(1, 1); + private readonly Dictionary ModbusTcpConnections = []; + private readonly Dictionary CcLinkConnections = []; + + public void Reset() + { + foreach (var client in ModbusTcpConnections.Values) + { + client.Dispose(); + } + ModbusTcpConnections.Clear(); + + foreach (var client in CcLinkConnections.Values) + { + client.Dispose(); + } + CcLinkConnections.Clear(); + } + + public async Task ConnectModbusTcp(string ipAddress, int port, byte unitId) + { + string key = $"{ipAddress}:{port}:{unitId}"; + await _connectLock.WaitAsync().ConfigureAwait(false); + + try + { + if (ModbusTcpConnections.TryGetValue(key, out var existingClient)) + { + return existingClient; + } + else + { + var client = new ModbusTcpClient(ipAddress, port, unitId); + + await client.ConnectAsync(); + + ModbusTcpConnections.Add(key, client); + + return client; + } + } + finally + { + _connectLock.Release(); + } + } + + public async Task ConnectCcLink(IeBasicClientOptions option) + { + string key = $"{option.Host}:{option.Port}:{option.NetworkNo}:{option.ModuleIoNo}:{option.MultidropNo}"; + await _connectLock.WaitAsync().ConfigureAwait(false); + + try + { + if (CcLinkConnections.TryGetValue(key, out var existingClient)) + { + return existingClient; + } + else + { + var client = new CcLinkIeBasicClient(option); + await client.ConnectAsync(); + + CcLinkConnections.Add(key, client); + return client; + } + } + finally + { + _connectLock.Release(); + } + } +} diff --git a/RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs b/RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs new file mode 100644 index 0000000..5fa9ddf --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptGlobalsManager.cs @@ -0,0 +1,231 @@ +using Microsoft.AspNetCore.SignalR; +using Microsoft.CodeAnalysis; +using OpenIddict.Client; +using RobotNet.OpenIddictClient; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Hubs; +using RobotNet.ScriptManager.Models; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptGlobalsManager +{ + public IDictionary Globals => _globals; + private readonly ConcurrentDictionary _globals = new(); + private readonly Dictionary _globalsTypes = []; + private readonly Dictionary variables = []; + private readonly IHubContext consoleHubContext; + private readonly ScriptRobotManager robotManager; + private readonly ScriptMapManager mapManager; + private readonly ScriptConnectionManager ConnectionManager; + private readonly IServiceScopeFactory scopeFactory; + + public ScriptGlobalsManager(IConfiguration configuration, OpenIddictClientService openIddictClient, IHubContext _consoleHubContext, ScriptConnectionManager connectionManager, IServiceScopeFactory _scopeFactory) + { + consoleHubContext = _consoleHubContext; + this.scopeFactory = _scopeFactory; + ConnectionManager = connectionManager; + + var robotManagerOptions = configuration.GetSection("RobotManager").Get() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + robotManager = new ScriptRobotManager(openIddictClient, robotManagerOptions.Url, robotManagerOptions.Scopes); + + var mapManagerOptions = configuration.GetSection("MapManager").Get() ?? throw new InvalidOperationException("OpenID configuration not found or invalid format."); + mapManager = new ScriptMapManager(openIddictClient, mapManagerOptions.Url, mapManagerOptions.Scopes); + } + + public void LoadVariables(IEnumerable variableSynctaxs) + { + _globals.Clear(); + _globalsTypes.Clear(); + variables.Clear(); + foreach (var v in variableSynctaxs) + { + variables.Add(v.Name, new(v.Name, v.Type, v.DefaultValue, v.PublicRead, v.PublicWrite)); + _globals.TryAdd(v.Name, v.DefaultValue); + _globalsTypes.TryAdd(v.Name, v.Type); + } + } + + public IEnumerable GetVariablesData() + { + var vars = new List(); + foreach (var v in variables.Values) + { + if (v.PublicRead) + { + vars.Add(new ScriptVariableDto(v.Name, v.TypeName, _globals[v.Name]?.ToString() ?? "null")); + } + } + + return vars; + } + + public Type? GetVariableType(string name) + { + if (_globalsTypes.TryGetValue(name, out var type)) + { + return type; + } + + return null; + } + + public IDictionary GetRobotNetTask(string name) + { + var globalRobotNet = new ScriptGlobalsRobotNetTask(new ScriptTaskLogger(consoleHubContext, name), robotManager, mapManager, ConnectionManager, scopeFactory); + + return ConvertGlobalsToDictionary(globalRobotNet); + } + + public IDictionary GetRobotNetMission(Guid missionId) + { + var globalRobotNet = new ScriptGlobalsRobotNetMission(missionId, new ScriptMissionLogger(consoleHubContext, missionId), robotManager, mapManager, ConnectionManager, scopeFactory); + + return ConvertGlobalsToDictionary(globalRobotNet); + } + + public void SetValue(string name, string value) + { + if (variables.TryGetValue(name, out var variable)) + { + if (variable.PublicWrite) + { + if (_globalsTypes.TryGetValue(name, out var type)) + { + try + { + var convertedValue = Convert.ChangeType(value, type); + _globals[name] = convertedValue; + } + catch (InvalidCastException) + { + throw new InvalidOperationException($"Cannot convert value '{value}' to type '{type.FullName}' for variable '{name}'."); + } + } + else + { + throw new KeyNotFoundException($"Variable type for '{name}' not found."); + } + } + else + { + throw new InvalidOperationException($"Variable '{name}' is not writable."); + } + } + else + { + throw new KeyNotFoundException($"Variable '{name}' not found."); + } + } + + public void ResetValue(string name) + { + if (variables.TryGetValue(name, out var variable)) + { + if (_globalsTypes.TryGetValue(name, out var type)) + { + _globals[name] = Convert.ChangeType(variable.DefaultValue, type); + } + else + { + throw new KeyNotFoundException($"Variable type for '{name}' not found."); + } + } + else + { + throw new KeyNotFoundException($"Variable '{name}' not found."); + } + } + + private static Dictionary ConvertGlobalsToDictionary(IRobotNetGlobals globals) + { + var robotnet = new Dictionary(); + var type = typeof(IRobotNetGlobals); + + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + robotnet.TryAdd(field.Name, field.GetValue(globals)); + } + + foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (prop.GetIndexParameters().Length == 0) + { + // Forward getter if available + if (prop.GetGetMethod() != null) + { + var getter = Delegate.CreateDelegate( + Expression.GetDelegateType([prop.PropertyType]), + globals, + prop.GetGetMethod()! + ); + robotnet.TryAdd($"get_{prop.Name}", getter); + } + // Forward setter if available + if (prop.GetSetMethod() != null) + { + var setter = Delegate.CreateDelegate( + Expression.GetDelegateType([prop.PropertyType, typeof(void)]), + globals, + prop.GetSetMethod()! + ); + robotnet.TryAdd($"set_{prop.Name}", setter); + } + } + else + { + // Handle indexers (properties with parameters) + var indexParams = prop.GetIndexParameters().Select(p => p.ParameterType).ToArray(); + + // Forward indexer getter if available + if (prop.GetGetMethod() != null) + { + var getterParamTypes = indexParams.Concat([prop.PropertyType]).ToArray(); + var getterDelegate = Delegate.CreateDelegate( + Expression.GetDelegateType(getterParamTypes), + globals, + prop.GetGetMethod()! + ); + robotnet.TryAdd($"get_{prop.Name}_indexer", getterDelegate); + } + + // Forward indexer setter if available + if (prop.GetSetMethod() != null) + { + var setterParamTypes = indexParams.Concat([prop.PropertyType, typeof(void)]).ToArray(); + var setterDelegate = Delegate.CreateDelegate( + Expression.GetDelegateType(setterParamTypes), + globals, + prop.GetSetMethod()! + ); + robotnet.TryAdd($"set_{prop.Name}_indexer", setterDelegate); + } + } + } + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + { + if (!method.IsSpecialName) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.FullName} {p.Name}")); + var args = string.Join(", ", method.GetParameters().Select(p => p.Name)); + var returnType = method.ReturnType == typeof(void) ? "void" : method.ReturnType.FullName; + + var paramTypes = string.Join(",", method.GetParameters().Select(p => p.ParameterType.FullName)); + var methodKey = $"{method.Name}({paramTypes})"; + + var del = Delegate.CreateDelegate( + Expression.GetDelegateType([.. method.GetParameters().Select(p => p.ParameterType), method.ReturnType]), + globals, + method + ); + // TODO: tôi muốn thay vì sử dụng tên phương thức, tôi muốn sử dụng tên phương thức với danh sánh kiểu dữ liệu của các tham số trong dấu ngoặc tròn + robotnet.TryAdd(methodKey, del); + } + } + return robotnet; + } +} diff --git a/RobotNet.ScriptManager/Services/ScriptMissionCreator.cs b/RobotNet.ScriptManager/Services/ScriptMissionCreator.cs new file mode 100644 index 0000000..e5819cb --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptMissionCreator.cs @@ -0,0 +1,63 @@ +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Data; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptMissionCreator(ScriptMissionManager missionManager, ScriptManagerDbContext scriptManagerDb) +{ + public async Task CreateMissionAsync(string name, IDictionary parameters) + { + if (!missionManager.ContainsMissionName(name)) + throw new Exception($"Mission {name} không tồn tại"); + + var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission + { + MissionName = name, + Parameters = System.Text.Json.JsonSerializer.Serialize(parameters), + CreatedAt = DateTime.UtcNow, + Status = MissionStatus.Idle, + }); + + await scriptManagerDb.SaveChangesAsync(); + + try + { + missionManager.Create(entry.Entity.Id, name, parameters); + return entry.Entity.Id; + } + catch (Exception ex) + { + scriptManagerDb.InstanceMissions.Remove(entry.Entity); + await scriptManagerDb.SaveChangesAsync(); + throw new Exception($"Failed to create mission: {ex.Message}", ex); + } + } + + public async Task CreateMissionAsync(string name, object[] parameters) + { + if (!missionManager.ContainsMissionName(name)) + throw new Exception($"Mission {name} không tồn tại"); + + var entry = scriptManagerDb.InstanceMissions.Add(new InstanceMission + { + MissionName = name, + Parameters = System.Text.Json.JsonSerializer.Serialize(parameters), + CreatedAt = DateTime.UtcNow, + Status = MissionStatus.Idle, + }); + + await scriptManagerDb.SaveChangesAsync(); + + try + { + missionManager.Create(entry.Entity.Id, name, parameters); + return entry.Entity.Id; + } + catch (Exception ex) + { + scriptManagerDb.InstanceMissions.Remove(entry.Entity); + await scriptManagerDb.SaveChangesAsync(); + throw new Exception($"Failed to create mission: {ex.Message}", ex); + } + } +} diff --git a/RobotNet.ScriptManager/Services/ScriptMissionManager.cs b/RobotNet.ScriptManager/Services/ScriptMissionManager.cs new file mode 100644 index 0000000..2b9a86c --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptMissionManager.cs @@ -0,0 +1,410 @@ +using Microsoft.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using RobotNet.Script; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Data; +using RobotNet.ScriptManager.Helpers; +using RobotNet.ScriptManager.Models; +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptMissionManager(ScriptGlobalsManager globalsManager, IServiceScopeFactory scopeFactory) +{ + public ProcessorState State { get; private set; } = ProcessorState.Idle; + private readonly Dictionary MissionDatas = []; + private readonly Dictionary>> Runners = []; + private readonly ConcurrentDictionary Missions = []; + private readonly ConcurrentQueue idleMissions = []; + private readonly ConcurrentQueue runningMissions = []; + + public void Reset() + { + if (State != ProcessorState.Idle && State != ProcessorState.Ready) + { + throw new InvalidOperationException("Cannot reset missions while the processor is running."); + } + MissionDatas.Clear(); + Runners.Clear(); + + foreach (var mission in Missions.Values) + { + mission.Dispose(); + } + Missions.Clear(); + + foreach (var mission in idleMissions) + { + mission.Dispose(); + } + idleMissions.Clear(); + + foreach (var mission in runningMissions) + { + mission.Dispose(); + } + runningMissions.Clear(); + + GC.Collect(); + + State = ProcessorState.Idle; + } + + public void LoadMissions(IEnumerable missionDatas) + { + if (State != ProcessorState.Idle && State != ProcessorState.Ready) + { + throw new InvalidOperationException("Cannot load missions while the processor is running."); + } + + MissionDatas.Clear(); + Runners.Clear(); + runningMissions.Clear(); + idleMissions.Clear(); + runningMissions.Clear(); + + foreach (var mission in missionDatas) + { + MissionDatas.Add(mission.Name, mission); + var script = CSharpScript.Create>(mission.Script, ScriptConfiguration.ScriptOptions, globalsType: typeof(ScriptMissionGlobals)); + Runners.Add(mission.Name, script.CreateDelegate()); + } + State = ProcessorState.Ready; + } + + public IEnumerable GetMissionDatas() => + [.. MissionDatas.Values.Select(m => new ScriptMissionDto(m.Name, m.Parameters.Select(p => new ScriptMissionParameterDto(p.Name, p.Type.FullName ?? p.Type.Name, p.DefaultValue?.ToString())), m.Code))]; + + public bool ContainsMissionName(string name) => Runners.ContainsKey(name); + + public void Create(Guid id, string name, IDictionary parameterStrings) + { + if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found."); + + var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token); + var parameters = new ConcurrentDictionary(); + bool hasCancellationToken = false; + + foreach (var param in missionData.Parameters) + { + if (param.Type == typeof(CancellationToken)) + { + if (hasCancellationToken) + { + throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined."); + } + hasCancellationToken = true; + parameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission + continue; + } + + if (!parameterStrings.TryGetValue(param.Name, out string? valueStr)) throw new ArgumentException($"Parameter '{param.Name}' not found in provided parameters."); + + if (CSharpSyntaxHelper.ResolveValueFromString(valueStr, param.Type, out var value) && value != null) + { + parameters.TryAdd(param.Name, value); + } + else + { + throw new ArgumentException($"Invalid value for parameter '{param.Name}': {valueStr}"); + } + } + + Create(id, name, parameters, cts); + } + + public void Create(Guid id, string name, object[] parameters) + { + if (!MissionDatas.TryGetValue(name, out var missionData)) throw new ArgumentException($"Mission data for '{name}' not found."); + if (parameters.Length != missionData.Parameters.Count()) + { + var count = missionData.Parameters.Count(p => p.Type == typeof(CancellationToken)); + if (count == 1) + { + if (parameters.Length != missionData.Parameters.Count() - count) + { + throw new ArgumentException($"Mission '{name}' expects {missionData.Parameters.Count()} parameters, but received {parameters.Length} without CancellationToken."); + } + } + else if (count != 0) + { + throw new ArgumentException($"Mission '{name}' just have one CancellationToken, but received {parameters.Length}."); + } + } + + var inputParameters = new ConcurrentDictionary(); + bool hasCancellationToken = false; + + var cts = CancellationTokenSource.CreateLinkedTokenSource(internalCts.Token); + int index = 0; + foreach (var param in missionData.Parameters) + { + if (param.Type == typeof(CancellationToken)) + { + if (hasCancellationToken) + { + throw new ArgumentException($"Mission '{name}' already has a CancellationToken parameter defined."); + } + hasCancellationToken = true; + inputParameters.TryAdd(param.Name, cts.Token); // Use the internal CancellationTokenSource for the mission + continue; + } + inputParameters.TryAdd(param.Name, parameters[index]); + index++; + } + Create(id, name, inputParameters, cts); + } + + public void Create(Guid id, string name, ConcurrentDictionary parameters, CancellationTokenSource cts) + { + if (!Runners.TryGetValue(name, out var runner)) throw new ArgumentException($"Mission '{name}' not found."); + + var robotnet = globalsManager.GetRobotNetMission(id); + var mission = new ScriptMission(id, name, runner, new ScriptMissionGlobals(globalsManager.Globals, robotnet, parameters), cts); + Missions.TryAdd(id, mission); + idleMissions.Enqueue(mission); + } + + private CancellationTokenSource internalCts = new(); + private Thread? thread; + public void Start(CancellationToken cancellationToken = default) + { + Stop(); // Ensure previous thread is stopped before starting a new one + ResetMissionDb(); + internalCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var token = internalCts.Token; + + thread = new Thread(() => + { + State = ProcessorState.Running; + while (!token.IsCancellationRequested || !runningMissions.IsEmpty) + { + var stopwatch = Stopwatch.StartNew(); + + MiningIdleMissionHandle(); + MiningRunningMissionHandle(); + + stopwatch.Stop(); + int elapsed = (int)stopwatch.ElapsedMilliseconds; + int remaining = 1000 - elapsed; + + // If execution time exceeds ProcessTime, add another cycle + if (elapsed > 900) + { + remaining += 1000; + } + + if (remaining > 0) + { + try + { + Thread.Sleep(remaining); + } + catch (ThreadInterruptedException) + { + break; + } + } + } + + State = ProcessorState.Ready; + }) + { + IsBackground = true, + Priority = ThreadPriority.Highest, + }; + thread.Start(); + } + + public bool Pause(Guid id) + { + if (Missions.TryGetValue(id, out var mission)) + { + return mission.Pause(); + } + + return false; + } + + public bool Resume(Guid id) + { + if (Missions.TryGetValue(id, out var mission)) + { + return mission.Resume(); + } + return false; + } + + public bool Cancel(Guid id, string reason) + { + if (Missions.TryGetValue(id, out var mission)) + { + return mission.Cancel(reason); + } + + return false; // Mission not found or not running + } + + public void Stop() + { + if (!idleMissions.IsEmpty || !runningMissions.IsEmpty) + { + var listWaitHandles = new List(); + while (idleMissions.TryDequeue(out var mission)) + { + mission.Cancel("Cancel by script mission manager is stoped"); + listWaitHandles.Add(mission.WaitHandle); + } + while (runningMissions.TryDequeue(out var mission)) + { + mission.Cancel("Cancel by script mission manager is stoped"); + listWaitHandles.Add(mission.WaitHandle); + } + + WaitHandle.WaitAll([.. listWaitHandles]); + } + + if (!internalCts.IsCancellationRequested) + { + internalCts.Cancel(); + } + + if (thread != null && thread.IsAlive) + { + thread.Interrupt(); + thread.Join(); + } + + internalCts.Dispose(); + thread = null; + } + + private void RemoveMission(ScriptMission mission) + { + mission.Dispose(); + Missions.TryRemove(mission.Id, out _); + } + + public void ResetMissionDb() + { + using var scope = scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var missions = dbContext.InstanceMissions.Where(m => m.Status == MissionStatus.Running + || m.Status == MissionStatus.Pausing + || m.Status == MissionStatus.Paused + || m.Status == MissionStatus.Canceling + || m.Status == MissionStatus.Resuming).ToList(); + foreach (var mission in missions) + { + mission.Log += $"{Environment.NewLine}{DateTime.UtcNow}: Mission Manager start, but instance mission has state {mission.Status}"; + mission.Status = MissionStatus.Error; + } + dbContext.SaveChanges(); + } + + private void MiningIdleMissionHandle() + { + using var scope = scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + int count = idleMissions.Count; + bool hasChanges = false; + + for (int i = 0; i < count; i++) + { + if (!idleMissions.TryDequeue(out var mission)) break; + + var dbMission = dbContext.InstanceMissions.Find(mission.Id); + if (dbMission == null) + { + RemoveMission(mission); + continue; // Skip if mission not found in database + } + + if (mission.Status == MissionStatus.Idle) + { + mission.Start(); + runningMissions.Enqueue(mission); + dbMission.Status = mission.Status; + } + else + { + RemoveMission(mission); + if (mission.Status == MissionStatus.Canceled) + { + dbMission.Status = MissionStatus.Canceled; + dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; + } + else + { + dbMission.Status = MissionStatus.Error; + dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}{Environment.NewLine}{DateTime.UtcNow}: Mission is not in idle state. [{mission.Status}]"; + } + hasChanges = true; + } + + } + if (hasChanges) + { + dbContext.SaveChanges(); + } + } + + private void MiningRunningMissionHandle() + { + using var scope = scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + bool hasChanges = false; + int count = runningMissions.Count; + for (int i = 0; i < count; i++) + { + if (!runningMissions.TryDequeue(out var mission)) break; + + var dbMission = dbContext.InstanceMissions.Find(mission.Id); + if (dbMission == null) + { + RemoveMission(mission); + continue; // Skip if mission not found in database + } + + switch (mission.Status) + { + case MissionStatus.Running: + case MissionStatus.Paused: + case MissionStatus.Canceling: + case MissionStatus.Resuming: + case MissionStatus.Pausing: + if (dbMission.Status != mission.Status) + { + dbMission.Status = mission.Status; + hasChanges = true; + } + runningMissions.Enqueue(mission); + break; + case MissionStatus.Completed: + case MissionStatus.Canceled: + case MissionStatus.Error: + dbMission.Status = mission.Status; + dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; + dbMission.StopedAt = DateTime.UtcNow; + hasChanges = true; + RemoveMission(mission); + break; // Handle these statuses in their respective methods + default: + dbMission.Status = MissionStatus.Error; + dbMission.Log += $"{Environment.NewLine} Wrong mission status on running: {mission.Status}"; + dbMission.Log += $"{Environment.NewLine}{mission.GetLog()}"; + hasChanges = true; + RemoveMission(mission); + continue; // Skip unknown statuses + } + } + if (hasChanges) + { + dbContext.SaveChanges(); + } + } +} diff --git a/RobotNet.ScriptManager/Services/ScriptStateManager.cs b/RobotNet.ScriptManager/Services/ScriptStateManager.cs new file mode 100644 index 0000000..1f385f2 --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptStateManager.cs @@ -0,0 +1,298 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Helpers; +using RobotNet.ScriptManager.Hubs; +using RobotNet.ScriptManager.Models; +using System.Diagnostics; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptStateManager(ScriptTaskManager taskManager, + ScriptMissionManager missionManager, + ScriptGlobalsManager globalsManager, + ScriptConnectionManager connectionManager, + IHubContext processorHubContext, + IHubContext hmiHubContext, + IHubContext consoleHubContext, + ILogger logger) : LoopService(500) +{ + public string StateMesssage { get; private set; } = ""; + public ProcessorState State { get; private set; } = ProcessorState.Idle; + public ProcessorRequest Request { get; private set; } = ProcessorRequest.None; + private readonly ConsoleLog consoleLog = new(consoleHubContext, logger); + + private readonly Mutex mutex = new(); + private CancellationTokenSource? runningCancellation; + + public bool Build(ref string message) + { + bool result = false; + + if (mutex.WaitOne(1000)) + { + if (Request != ProcessorRequest.None) + { + message = $"Không thể thực hiện build vì Processor đang thực hiện {Request}"; + } + else if (State == ProcessorState.Running) + { + message = "Không thể thực hiện build vì Processor đang Running}"; + } + else + { + result = true; + SetRequest(ProcessorRequest.Build); + } + + mutex.ReleaseMutex(); + } + else + { + message = "Không thể thực hiện build vì request timeout"; + } + + return result; + } + + public bool Run(ref string message) + { + bool result = false; + + if (mutex.WaitOne(1000)) + { + if (Request != ProcessorRequest.None) + { + message = $"Không thể thực hiện run vì Processor đang thực hiện {Request}"; + } + else if (State != ProcessorState.Ready) + { + message = $"Không thể thực hiện run vì Processor đang ở trạng thái {State}, không phải Ready"; + } + else + { + result = true; + SetRequest(ProcessorRequest.Run); + } + + mutex.ReleaseMutex(); + } + else + { + message = "Không thể thực hiện run vì request timeout"; + } + + return result; + } + + public bool Stop(ref string message) + { + bool result = false; + + if (mutex.WaitOne(1000)) + { + if (Request != ProcessorRequest.None) + { + message = $"Không thể thực hiện stop vì Processor đang thực hiện {Request}"; + } + else if (State != ProcessorState.Running) + { + message = $"Không thể thực hiện stop vì Processor đang ở trạng thái {State}, không phải Running"; + } + else + { + result = true; + SetRequest(ProcessorRequest.Stop); + } + + mutex.ReleaseMutex(); + } + else + { + message = "Không thể thực hiện stop vì request timeout"; + } + + return result; + } + + public bool Reset(ref string message) + { + bool result = false; + + if (mutex.WaitOne(1000)) + { + if (Request != ProcessorRequest.None) + { + message = $"Không thể thực hiện reset vì Processor đang thực hiện {Request}"; + } + else if (State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError && State != ProcessorState.Idle) + { + message = $"Không thể thực hiện reset vì Processor đang ở trạng thái {State}, không phải Ready hoặc Error hoặc BuildError"; + } + else + { + result = true; + SetRequest(ProcessorRequest.Reset); + } + + mutex.ReleaseMutex(); + } + else + { + message = "Không thể thực hiện reset vì request timeout"; + } + + return result; + } + + private void SetRequest(ProcessorRequest request) + { + if (Request == request) return; + + Request = request; + _ = Task.Factory.StartNew(async () => + { + await processorHubContext.Clients.All.SendAsync("RequestChanged", Request); + await hmiHubContext.Clients.All.SendAsync("RequestChanged", Request); + }); + } + + private void SetState(ProcessorState state) + { + if (State == state) return; + + State = state; + _ = Task.Factory.StartNew(async () => + { + await processorHubContext.Clients.All.SendAsync("StateChanged", State); + await hmiHubContext.Clients.All.SendAsync("StateChanged", State); + }); + } + + protected override async Task BeforExecuteAsync(CancellationToken cancellationToken) + { + missionManager.ResetMissionDb(); + SetRequest(ProcessorRequest.Build); + await base.BeforExecuteAsync(cancellationToken); + } + + protected override void Execute(CancellationToken stoppingToken) + { + switch (State) + { + case ProcessorState.Idle: + case ProcessorState.BuildError: + case ProcessorState.Error: + if (Request == ProcessorRequest.Build) + { + SetState(ProcessorState.Building); + SetRequest(ProcessorRequest.None); + BuildHandler(); + } + else if (Request == ProcessorRequest.Reset) + { + missionManager.Reset(); + taskManager.Reset(); + connectionManager.Reset(); + SetState(ProcessorState.Idle); + SetRequest(ProcessorRequest.None); + } + break; + case ProcessorState.Ready: + if (Request == ProcessorRequest.Build) + { + SetState(ProcessorState.Building); + SetRequest(ProcessorRequest.None); + BuildHandler(); + } + else if (Request == ProcessorRequest.Run) + { + SetState(ProcessorState.Running); + SetRequest(ProcessorRequest.None); + RunHandler(); + } + else if (Request == ProcessorRequest.Reset) + { + missionManager.Reset(); + taskManager.Reset(); + connectionManager.Reset(); + SetState(ProcessorState.Idle); + SetRequest(ProcessorRequest.None); + } + break; + case ProcessorState.Running: + if (Request == ProcessorRequest.Stop) + { + connectionManager.Reset(); + SetState(ProcessorState.Stopping); + SetRequest(ProcessorRequest.None); + StopHandler(); + } + break; + case ProcessorState.Building: + case ProcessorState.Stopping: + case ProcessorState.Starting: + default: + SetRequest(ProcessorRequest.None); + break; + } + } + + private void BuildHandler() + { + _ = Task.Factory.StartNew(() => + { + consoleLog.LogInfo($"Start build all scripts"); + var watch = new Stopwatch(); + watch.Start(); + var code = ScriptConfiguration.GetScriptCode(); + + string error = string.Empty; + try + { + if (CSharpSyntaxHelper.VerifyScript(code, out error, out var variables, out var tasks, out var missions)) + { + globalsManager.LoadVariables(variables); + taskManager.LoadTasks(tasks); + missionManager.LoadMissions(missions); + watch.Stop(); + consoleLog.LogInfo($"Build all scripts successfully in {watch.ElapsedMilliseconds} ms"); + SetState(ProcessorState.Ready); + return; + } + } + catch (Exception ex) + { + error = ex.ToString(); + } + + watch.Stop(); + SetState(ProcessorState.BuildError); + consoleLog.LogError(error); + + }); + } + + private void RunHandler() + { + _ = Task.Factory.StartNew(() => + { + runningCancellation = new(); + taskManager.StartAll(runningCancellation.Token); + missionManager.Start(runningCancellation.Token); + SetState(ProcessorState.Running); + + }); + } + + private void StopHandler() + { + _ = Task.Factory.StartNew(() => + { + runningCancellation?.Cancel(); + taskManager.StopAll(); + missionManager.Stop(); + SetState(ProcessorState.Ready); + }); + } + +} diff --git a/RobotNet.ScriptManager/Services/ScriptTaskManager.cs b/RobotNet.ScriptManager/Services/ScriptTaskManager.cs new file mode 100644 index 0000000..7deefac --- /dev/null +++ b/RobotNet.ScriptManager/Services/ScriptTaskManager.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.SignalR; +using RobotNet.Script.Shares; +using RobotNet.ScriptManager.Helpers; +using RobotNet.ScriptManager.Hubs; +using RobotNet.ScriptManager.Models; + +namespace RobotNet.ScriptManager.Services; + +public class ScriptTaskManager(ScriptGlobalsManager globalsManager, IHubContext scriptHub) +{ + private readonly List ScriptTaskDatas = []; + private readonly Dictionary ScriptTasks = []; + + public void Reset() + { + ScriptTaskDatas.Clear(); + foreach (var task in ScriptTasks.Values) + { + task.Dispose(); + } + ScriptTasks.Clear(); + GC.Collect(); + } + + public void LoadTasks(IEnumerable tasks) + { + Reset(); + ScriptTaskDatas.AddRange(tasks); + foreach (var task in tasks) + { + ScriptTasks.Add(task.Name, new ScriptTask(task, ScriptConfiguration.ScriptOptions, globalsManager.Globals, globalsManager.GetRobotNetTask(task.Name))); + } + } + + public IEnumerable GetTaskDatas() => [.. ScriptTaskDatas.Select(t => new ScriptTaskDto(t.Name, t.Interval, t.Code))]; + + public void StartAll(CancellationToken cancellationToken = default) + { + foreach (var task in ScriptTasks.Values) + { + task.Start(cancellationToken); + } + } + + public void StopAll() + { + foreach (var task in ScriptTasks.Values) + { + task.Stop(); + } + } + + public IDictionary GetTaskStates() + { + return ScriptTasks.ToDictionary(task => task.Key, task => !task.Value.Paused); + } + + public bool Pause(string name) + { + if (ScriptTasks.TryGetValue(name, out var task)) + { + task.Pause(); + _ = Task.Factory.StartNew(async Task () => + { + await scriptHub.Clients.All.SendAsync("TaskPaused", name); + }, TaskCreationOptions.LongRunning); + return true; + } + else + { + return false; + } + } + + public bool Resume(string name) + { + if(ScriptTasks.TryGetValue(name, out var task)) + { + task.Resume(); + _ = Task.Factory.StartNew(async Task () => + { + await scriptHub.Clients.All.SendAsync("TaskResumed", name); + }, TaskCreationOptions.LongRunning); + return true; + } + else + { + return false; + } + } +} diff --git a/RobotNet.ScriptManager/appsettings.json b/RobotNet.ScriptManager/appsettings.json new file mode 100644 index 0000000..5a89ba3 --- /dev/null +++ b/RobotNet.ScriptManager/appsettings.json @@ -0,0 +1,44 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=172.20.235.170;Database=RobotNet.Scripts;User Id=sa;Password=robotics@2022;TrustServerCertificate=True;MultipleActiveResultSets=true" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "System.Net.Http.HttpClient": "Warning", + "OpenIddict.Validation.OpenIddictValidationDispatcher": "Warning", + "OpenIddict.Client.OpenIddictClientDispatcher": "Warning", + "Microsoft.EntityFrameworkCore.Database": "Warning" + } + }, + "AllowedHosts": "*", + "OpenIddictClientProviderOptions": { + "Issuer": "https://localhost:7061/", + "Audiences": [ + "robotnet-script-manager" + ], + "ClientId": "robotnet-script-manager", + "ClientSecret": "05594ECB-BBAE-4246-8EED-4F0841C3B475", + "Scopes": [ + "robotnet-robot-api", + "robotnet-map-api" + ] + }, + "RobotManager": { + "Url": "https://localhost:7179", + "Scopes": [ + "robotnet-robot-api" + ] + }, + "MapManager": { + "Url": "https://localhost:7177", + "Scopes": [ + "robotnet-map-api" + ] + }, + "Dashboard": { + "UpdateTimeMilliSeconds": 5000, + "CycleDate": 7 + } +} \ No newline at end of file diff --git a/RobotNet.ScriptManager/nlog.config b/RobotNet.ScriptManager/nlog.config new file mode 100644 index 0000000..c2ea054 --- /dev/null +++ b/RobotNet.ScriptManager/nlog.config @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.ScriptManager/readme b/RobotNet.ScriptManager/readme new file mode 100644 index 0000000..fb8a69e --- /dev/null +++ b/RobotNet.ScriptManager/readme @@ -0,0 +1,5 @@ +Tạo thư mục "scripts" và "dlls" trong thư mục build (RobotNet.ScriptManager\bin\Debug\net9.0) +Sao chép RobotNet.Script.dll vào "dlls" +Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Linq.Expressions.dll vào thư mục "dlls" +Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Private.CoreLib.dll vào thư mục "dlls" +Sao chép C:\Program Files\dotnet\shared\Microsoft.NETCore.App\9.0.7\System.Runtime.dll vào thư mục "dlls" diff --git a/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.Expressions.dll b/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.Expressions.dll new file mode 100644 index 0000000000000000000000000000000000000000..a967b7aaa0e3d820a19346ca2b9282febdc84fc5 GIT binary patch literal 10240 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 + + + RobotNet.Script.Expressions + + + + + Quản lý các thuộc tính của một phần tử trong bản đồ. + + + + + Gets or sets a value indicating whether the resource is currently open. + + + + + Gets a dictionary that maps string keys to boolean values. + + + + + Gets a dictionary that maps string keys to double values. + + + + + Gets a dictionary that maps string keys to integer values. + + + + + Gets a dictionary that maps string keys to string values. + + + + + + + + + + + + + + + Trạng thái hiện tại của robot, bao gồm thông tin về vị trí, trạng thái hoạt động, pin, v.v. + + + + + + + + + + + + Trạng thái hiện tại của robot, bao gồm thông tin về vị trí, trạng thái hoạt động, pin, v.v. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.dll b/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.dll new file mode 100644 index 0000000000000000000000000000000000000000..fe08397ce59dea218f7ce732bc72e2acf8ee68bc GIT binary patch literal 35840 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 VaV9|ZOHcnFY~z2c{(m+C{}*i9q$L0V literal 0 HcmV?d00001 diff --git a/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml b/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml new file mode 100644 index 0000000..44cd16c --- /dev/null +++ b/RobotNet.ScriptManager/wwwroot/dlls/RobotNet.Script.xml @@ -0,0 +1,1075 @@ + + + + RobotNet.Script + + + +

+ Client CC‑Link IE Basic (SLMP over TCP/UDP) dùng để kết nối PC ↔ PLC Mitsubishi qua Ethernet thường, + hỗ trợ đọc/ghi device, polling chu kỳ và gửi lệnh SLMP thô. + + + + + Trạng thái đã kết nối phiên transport (TCP/UDP) tới PLC hay chưa. + + + + + Số lần thử gửi lại khi lỗi tạm thời/timeout. + + + + + Ngắt kết nối transport và giải phóng tài nguyên liên quan. + + Token hủy bất đồng bộ. + + + + Đọc mảng bit (X/Y/M/L/S/B...) từ PLC. + + Loại device code. + Địa chỉ bắt đầu. + Số bit cần đọc. + Token hủy. + Mảng giá trị bit. + + + + Ghi mảng bit (X/Y/M/L/S/B...) vào PLC. + + Loại device code. + Địa chỉ bắt đầu. + Dãy giá trị bit cần ghi. + Token hủy. + + + + Đọc mảng word (D/W/R/ZR/B...) từ PLC. + + Loại device code. + Địa chỉ bắt đầu (đơn vị word). + Số word cần đọc. + Token hủy. + Mảng word (UInt16). + + + + Ghi mảng word (D/W/R/ZR/B...) vào PLC. + + Loại device code. + Địa chỉ bắt đầu (đơn vị word). + Dãy word cần ghi. + Token hủy. + + + + Đọc mảng double‑word (UInt32) từ PLC (áp dụng cho vùng hỗ trợ dword). + + Loại device code. + Địa chỉ bắt đầu (đơn vị dword). + Số dword cần đọc. + Token hủy. + Mảng dword (UInt32). + + + + Ghi mảng double‑word (UInt32) vào PLC (áp dụng cho vùng hỗ trợ dword). + + Loại device code. + Địa chỉ bắt đầu (đơn vị dword). + Dãy dword cần ghi. + Token hủy. + + + + Đọc ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần. + + Danh sách (device, address) cần đọc. + Token hủy. + Mảng word theo thứ tự điểm yêu cầu. + + + + Ghi ngẫu nhiên nhiều điểm word rời rạc (mixed areas) trong một lần. + + Danh sách (device, address) cần ghi. + Giá trị word theo thứ tự điểm. + Token hủy. + + + + Sự kiện bắn ra sau mỗi chu kỳ polling hoàn tất. + + + + + Bắt đầu polling chu kỳ một hoặc nhiều vùng device. + + Tùy chọn polling (chu kỳ, vùng, giới hạn kích thước). + Token hủy. + + + + Dừng polling chu kỳ. + + Token hủy. + + + + Lấy trạng thái liên kết/SLMP gần nhất (end code, thống kê thời gian phản hồi...). + + Token hủy. + Thông tin trạng thái IE Basic. + + + + Kiểm tra liên lạc PLC (ví dụ node monitor) ở mức nhanh/gọn. + + Token hủy. + true nếu phản hồi hợp lệ, ngược lại false. + + + + Truy vấn thông tin nhận dạng module/CPU (nếu PLC cho phép). + + Token hủy. + Thông tin định danh CPU/module. + + + + Gửi lệnh SLMP thô (mở rộng/đặc thù) và nhận phản hồi. + + Mã lệnh SLMP. + Payload SLMP theo định dạng frame chọn (3E/4E). + Token hủy. + Phản hồi SLMP gồm end code và payload. + + + + Loại transport cho CC‑Link IE Basic (UDP hoặc TCP). + + + + Giao tiếp qua UDP (thường dùng cho IE Basic). + + + Giao tiếp qua TCP. + + + + Định dạng frame SLMP. + + + + Khung 3E (Format 3E). + + + Khung 4E (Format 4E). + + + + Device code (vùng thiết bị) cấp ứng dụng. + + + + Input (bit). + + + Output (bit). + + + Internal relay (bit). + + + Latch relay (bit). + + + Link relay (bit/word tùy CPU). + + + Step relay (bit). + + + Data register (word). + + + Link register (word). + + + File register (word). + + + Extended file register (word). + + + + Tham số logic phía local (PC/sender) khi gửi SLMP. + + Số mạng logic. + Địa chỉ I/O module logic (thường 0x03FF). + Số đa điểm (nếu dùng). + Số trạm logic. + + + + Tham số logic phía local (PC/sender) khi gửi SLMP. + + Số mạng logic. + Địa chỉ I/O module logic (thường 0x03FF). + Số đa điểm (nếu dùng). + Số trạm logic. + + + Số mạng logic. + + + Địa chỉ I/O module logic (thường 0x03FF). + + + Số đa điểm (nếu dùng). + + + Số trạm logic. + + + + Cấu hình đầu xa: địa chỉ IP/Port và tham số logic đích cho SLMP. + + Địa chỉ IP hoặc hostname PLC/module. + Cổng SLMP (ví dụ 5007). + Số mạng logic đích. + Địa chỉ I/O module logic đích. + Số đa điểm (nếu dùng). + Số trạm logic đích. + + + + Cấu hình đầu xa: địa chỉ IP/Port và tham số logic đích cho SLMP. + + Địa chỉ IP hoặc hostname PLC/module. + Cổng SLMP (ví dụ 5007). + Số mạng logic đích. + Địa chỉ I/O module logic đích. + Số đa điểm (nếu dùng). + Số trạm logic đích. + + + Địa chỉ IP hoặc hostname PLC/module. + + + Cổng SLMP (ví dụ 5007). + + + Số mạng logic đích. + + + Địa chỉ I/O module logic đích. + + + Số đa điểm (nếu dùng). + + + Số trạm logic đích. + + + + Tùy chọn khởi tạo/kết nối IE Basic client. + + + + + Tùy chọn khởi tạo/kết nối IE Basic client. + + + + Transport: UDP hoặc TCP. + + + Định dạng frame SLMP (3E/4E). + + + Tham số logic phía local. + + + Timeout cho mỗi yêu cầu. + + + Số lần retry khi lỗi tạm thời. + + + Tự động reconnect nếu mất liên kết. + + + + Quy ước ghép cặp word trong DWord/Float (true: little‑endian word order). + + + + + Tùy chọn cho polling chu kỳ các vùng device. + + + + Khoảng thời gian giữa hai lần poll. + + + Danh sách vùng cần poll (device, địa chỉ bắt đầu, số lượng). + + + Giới hạn số word tối đa mỗi chu kỳ (để tránh gói quá lớn). + + + Gom vùng theo từng loại device để tối ưu số gói. + + + + Dữ liệu trả về sau mỗi chu kỳ polling. + + + + Thời điểm khung dữ liệu được cập nhật. + + + Mảng bit (nếu có vùng bit được cấu hình). + + + Mảng word (nếu có vùng word được cấu hình). + + + Mảng dword (nếu có vùng dword được cấu hình). + + + + Trạng thái liên kết/SLMP để chẩn đoán và theo dõi. + + + + Liên kết hiện “đang lên” (có phản hồi) hay không. + + + Số lỗi liên tiếp gần nhất. + + + End code SLMP của lỗi gần nhất (nếu có). + + + Mô tả lỗi gần nhất (nếu có). + + + Thời gian phản hồi trung bình ước tính. + + + Thời gian phản hồi lớn nhất quan sát. + + + + Thông tin nhận dạng CPU/module (nếu PLC cho phép truy vấn). + + + + Dòng CPU (ví dụ iQ‑R, iQ‑F). + + + Model cụ thể (ví dụ R04ENCPU). + + + Phiên bản firmware. + + + Nhà sản xuất (thường là Mitsubishi Electric). + + + Thông tin bổ sung khác (nếu có). + + + + Mã lệnh SLMP rút gọn (có thể mở rộng khi hiện thực adapter). + + + + Đọc device theo dải liên tục (bit/word). + + + Ghi device theo dải liên tục (bit/word). + + + Đọc ngẫu nhiên nhiều điểm. + + + Ghi ngẫu nhiên nhiều điểm. + + + Node monitor / kiểm tra liên lạc. + + + Truy vấn thông tin thiết bị/PLC. + + + + Mã end code SLMP (hoàn thành/thất bại), gồm cả mã nội bộ ánh xạ lỗi transport/timeout. + + + + Hoàn thành thành công. + + + Lệnh không hợp lệ. + + + Truy cập bị từ chối. + + + Lỗi phạm vi device. + + + Thiết bị bận. + + + Quá thời gian chờ (gán nội bộ). + + + Lỗi transport/socket (gán nội bộ). + + + Lỗi không xác định (gán nội bộ). + + + + Phản hồi SLMP thô trả về từ . + + + + End code của phản hồi. + + + Payload nhị phân trả về (theo frame 3E/4E). + + + Thời điểm nhận gói. + + + + Quản lý kết nối với device khác. + + + + + Tạo kết nối Modbus TCP đến một thiết bị với địa chỉ IP, cổng và ID đơn vị cụ thể. + + + + + + + + + + + + + + + + Xuất thông tin ghi log cho các thành phần trong RobotNet. + + + + + Xuất thông tin ghi log với mức độ thông tin. + + + + + + Xuất thông tin ghi log với mức độ cảnh báo. + + + + + + Xuất thông tin ghi log với mức độ lỗi. + + + + + + Service quản lý các phần tử trong bản đồ. + + + + + Trả về phần tử theo tên trong bản đồ. + + + + + + + + Retrieves a node from the specified map by its name. + + This method performs an asynchronous operation to locate a node within the specified map. + Ensure that the map and name parameters are valid and non-empty before calling this method. + The identifier of the map from which to retrieve the node. Cannot be null or empty. + The name of the node to retrieve. Cannot be null or empty. + A task that represents the asynchronous operation. The task result contains the node matching the specified name + within the given map, or if no such node exists. + + + + Tìm kiếm các phần tử trong bản đồ theo biểu thức. + + + + + + + + Tìm kiếm các phần tử trong bản đồ theo biểu thức. + + + + + + + + + Thông tin về một nút trong bản đồ. + + + + + Id của nút. + + + + + Id của map chứa nút này. + + + + + Tên của nút + + + + + Tọa độ trục hoành của nút trong không gian 2D. + + + + + Tọa độ trục tung của nút trong không gian 2D. + + + + + Hướng của nút trong không gian 2D, tính bằng độ. + + + + + Quản lý thông tin của một phần tử trong bản đồ. + + + + + Id của phần tử. + + + + + Id model của phần tử này. + + + + + Tên của model mà phần tử này thuộc về. + + + + + Id của nút mà phần tử này thuộc về. + + + + + Tên của phần tử. + + + + + Tạo độ trục hoành tương dối của phần từ so với nút mà nó thuộc về. + + + + + Tọa độ trục tung tương đối của phần từ so với nút mà nó thuộc về. + + + + + Tên của nút mà phần tử này thuộc về. + + + + + Tọa độ trục hoành của nút trong không gian 2D. + + + + + Tọa độ trục tung của nút trong không gian 2D. + + + + + Hướng của phần tử trong không gian 2D, tính bằng độ. + + + + + Các thuộc tính của phần tử, bao gồm các thuộc tính bool, double, int và string. + + + + + Lưu các thay đổi của thuộc tính phần tử vào cơ sở dữ liệu + + + + + + Quản ký kết nối Modbus TCP tới 1 thiết bị. + + + + + Trạng thái kết nối hiện tại với thiết bị Modbus TCP. + + + + + Đọc các đầu vào rời rạc (Discrete Inputs) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + + Đọc các cuộn dây (Coils) từ thiết bị Modbus TCP. + + + + + + + + + Đọc các thanh ghi giữ (Holding Registers) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + + Đọc các thanh ghi đầu vào (Input Registers) từ thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + + Ghi viết một cuộn dây (Coil) đơn lẻ đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + Ghi viết một thanh ghi đơn lẻ đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + Ghi viết nhiều cuộn dây (Coils) đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + Ghi viết nhiều thanh ghi (Holding Registers) đến thiết bị Modbus TCP với hỗ trợ hủy bỏ. + + + + + + + + Đọc và ghi viết nhiều thanh ghi (Holding Registers) trong một lần gọi với hỗ trợ hủy bỏ. + + + + + + + + + + + Service điểu khiển robot trong hệ thống RobotNet. + + + + + Di chuyển robot đến một nút trong bản đồ với action kết thúc. + + + + + + + + + Di chuyển robot đến một nút trong bản đồ với action kết thúc. + + + + + + + + + + Di chuyển robot đến một nút trong bản đồ với action kết thúc. + + + + + + + + + Di chuyển robot đến một nút trong bản đồ với action kết thúc. + + + + + + + + + + Di chuyển robot đến một nút trong bản đồ mà không cần action kết thúc. + + + + + + + + + Di chuyển robot đến một nút trong bản đồ mà không cần action kết thúc. + + + + + + + + Hủy bỏ hành động di chuyển hiện tại của robot, nếu có. + + + + + + Thực hiện một hành động ngay lập tức trên robot mà không cần chờ đợi. + + + + + + + + Lấy trạng thái của robot + + + + + + Waits for the system to reach a ready state. + + A token to monitor for cancellation requests. If the token is canceled, the operation will be aborted. + A task that represents the asynchronous operation. The task result is if the system + reaches a ready state; otherwise, if the operation is canceled or the system fails to + become ready. + + + + Trả về danh sách các tải trọng hiện tại của robot, bao gồm ID, loại, vị trí và trọng lượng. + + + + + + Chức năng di chuyển robot giả lập theo tọa độ (x, y) trong không gian 2D. + + + + + + + + + Chức năng xoay robot giả lập theo một góc nhất định (đơn vị: độ). + + + + + + + + Service quản lý các robot trong hệ thống RobotNet. + + + + + Trả về đối tượng điểu khiển robot theo ID. + + + + + + + Lấy trạng thái của một robot theo ID. + + + + + + + Tìm kiếm các robot trong bản đồ theo tên và mô hình. + + + + + + + + Tìm kiếm các robot trong bản đồ theo tên và mô hình. + + + + + + + + + Regulates if the action is allowed to be executed during movement and/or parallel to other actions. + + + + + Action can happen in parallel with others, including movement. + + + + + Action can happen simultaneously with others, but not while moving. + + + + + No other actions can be performed while this action is running. + + + + + Mô tả một hành động của robot. + + + + + + + Mô tả một hành động của robot. + + + + + + + + + + + + + Danh sách các tham số của hành động robot. + + + + + Dữ liệu trả về từ các hành động của robot, bao gồm thông tin về thành công hay thất bại và thông điệp mô tả. + + + + + + + Dữ liệu trả về từ các hành động của robot, bao gồm thông tin về thành công hay thất bại và thông điệp mô tả. + + + + + + + + + + + + + Thông tin về tải trọng của robot, bao gồm ID, loại, vị trí và trọng lượng. + + + + + + + + + Thông tin về tải trọng của robot, bao gồm ID, loại, vị trí và trọng lượng. + + + + + + + + + + + + + + + + + + + + + Service để đọc và ghi dữ liệu từ các thiết bị Unix. + + + + + Đọc dữ liệu từ thiết bị Unix theo tên và độ dài yêu cầu. + + + + + + + + Ghi dữ liệu vào thiết bị Unix theo tên. + + + + + + + Trạng thái trả về mỗi step trong quá trình thực hiện nhiệm vụ. + + + + + + + Trạng thái trả về mỗi step trong quá trình thực hiện nhiệm vụ. + + + + + + + + + + + + + Khai báo một nhiệm vụ trong hệ thống RobotNet. + + + + + + Khai báo một nhiệm vụ trong hệ thống RobotNet. + + + + + + Cho phép nhiệm vụ này có thể chạy nhiều lần hay không. + + + + + Thuộc tính để đánh dấu một phương thức là một tác vụ định kỳ trong hệ thống RobotNet. + + + + + + + Thuộc tính để đánh dấu một phương thức là một tác vụ định kỳ trong hệ thống RobotNet. + + + + + + + Thời gian định kỳ để thực hiện tác vụ, tính bằng giây. + + + + + Cho phép tác vụ này tự động bắt đầu khi hệ thống khởi động hay không. + + + + + Khai báo biến được phép đọc giá trị từ ngoài hệ thống RobotNet Script + + + + + + Khai báo biến được phép đọc giá trị từ ngoài hệ thống RobotNet Script + + + + + + Khai báo biến được phép ghi giá trị từ ngoài hệ thống RobotNet Script + + + + diff --git a/RobotNet.ScriptManager/wwwroot/dlls/System.Collections.dll b/RobotNet.ScriptManager/wwwroot/dlls/System.Collections.dll new file mode 100644 index 0000000000000000000000000000000000000000..6b459419f2b63bbe4b573915f84668dc5ce41612 GIT binary patch literal 56592 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 literal 0 HcmV?d00001 diff --git a/RobotNet.ScriptManager/wwwroot/dlls/System.Linq.Expressions.dll b/RobotNet.ScriptManager/wwwroot/dlls/System.Linq.Expressions.dll new file mode 100644 index 0000000000000000000000000000000000000000..1117bfb9130a784adc5e88d8c2b8f38242cab5aa GIT binary patch literal 63792 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�$xNI#6gHV?JXmV^c@c-^ZBMiR326X2#Y|l&h>{Dp6K4 z$(xc-G4AZ#%9`@dWWS2hN}qoE|z($$p=SMr&aM%c{Q%9z!SExVDvg>i2;s#VdQFvvKTaV}%1 zJ6Sfe+}xe~H+LrwRywDfPMR#1LyRIw`5YT0Z*v(N8M89jGJ{jkC^W{UHnH5y`evoi zq_l3&q_~?H_cFG!USx3&8OJgP@kNeJI7TjCp-XGneH?#@iV;F}8B(*yE^g=N(6uA;!JOQBK71 zgrO6t23i?K4rzjnjX5+Q-p<&ZLvc5;yq7VrA8A?{^ZK)A#wJE<0BN!Y6dEU|h6a%T z&;Y98CYGBua(fLV%RI)$ft;&BTx!O-jG;m7Z7|~y!q5DnSq!~MrZCGw)%$md=7+WS$o;OVXR4xT$a4PBZrjku7W8O59 z=T0Mih-K?E(lj$#(@D-^Y@ALuw==dVeIbSBF}5;}En@#g?33kYmaQ3-w>-uWV>4sR z49Z&z%hu^6Tc>j=PA@bHQyWhwZ_TXFnn|HeGdaasgw2e3vq^58O_r@J=lzZ}Ax7&A zmKj?Zv(6+<3u9;w$(!a-+!mJivfRpY-dPmd%-F(colTlN#-_6=pUo_{o=tgcWlhsu z&gWbXoy%#>Ei}$d9a~JP>}3=sB4Y9W{htA_r#%9L6`J`!P zY+($Qv4*jwjN-PGQBJIK_Q04|PMRi`n;Bcm*^>3vIV4->kWJP()Vo5AEv#u}Icovw zn-*~17O?*cl0yqQhm5TYNz=NJ{Zx`ZuaY!*m7F?jS{SV=3bm@vFtDDooX2vgn)T-r zhR($s15;aAZe`i3BTZ;Ad23lrHcd-7Ck=$52GZo6$8j06&L=r*DeIS#O*5mljALCu zm~{b#HnH5yatmW#Bl~G2n--Rvms436;l_8Lewc({wHQY-QQHo;22Kwq$I* zfi=G;4E>%gTUefe6KlNY+x!xzpLi|^=mAFIPNEM|G^BW($RvsmI2lXDUxP`9l!?=s z-gqzj@i<8vg!8r`I4>K4(9t+m8;5fmoJfgDID4B6%TsY;Q;3&L%@jSwY`m9zE@Fmp zvUU!>61D&*I19xIIAO^V^`f6xgtMH*IB#2qv$_l6`C^>vtb+F|#8CXjk`r;JG7Kjw z!^N%mt1EXR#ryELdLBTk4~j83MHwp|Me2`>@#2rTvi}p#^qxabwjd{4MLteYP8Qp7 zt^Ze?0__x2aO!p{&QOFnJ2k!o{#I*30Qk446kwoxH{jp1vVnz3eSnR9NVYXOkl*Sx z7^KfMsmny*UW-B>3!DO6!Ep~IkmjczWYcJ! z2JDeR^4Rp5zjb?WJ<4%JQ%DYY=)`#~YnosZix-bOM#AJEObQDu+9~w;}XU%3VOE zQL?fe-2DOQRVwZMt8|r(Dz)lADsKClQ9VlKP1*m{^$GaDBmEhmisi1!o!p<@^`PRq zOX2Q~Uv#7XFtOWKV0JR~Xw_D`hm z0=iRlx2(!fSBv_dswMZBa@R(>6-j+gCmK_#HC6BG)a`v}24qmrS9;~SL+AaD#w}kI zMV6{&l)cK;u2|Cd;C`|q{UGF}JTBB&Q@J6hDrnm{~pDFMTb)eT=0gN&)2KvlPfYIhEV2pV= zFy6cpm|$KF>|kCC>}XyO>}1{mOg4WH>}=i$Ou^}=iMt!?fZfg8fa&HPz@WJv*u%UF z*wefR*vq^R*xP&n*avrSOmTww2Vjo*2ylS;7;vEZ1aOe~N8n)dDc}(E8Q@U!Prwt+ zKLbaan}MUuEx^&{R^S-(FTk<5YlN0H{|cOFz66|P?gUOYUj^oyuLGx;yMWWow}7Xa z?*NO;-M|^{{>uVegmvD4*_ebiMzc~u{fH?-Z!5jyC z(98usVom@)YEA+^X66GQH%|dRVNL-)X-)|>oRr74%n`SZaEwdE3#|#7CGv@>UW|jl@nG1le=0f17W)<-7=DEOwW-ag^W*7XmxW<%S_rfS7aS3Sg?d7`2M&-|0td>g zfhWpqfy3qXz?0++z%laoz_Id1;5d0RaJ*axoGfnx=F2;Pr^@xfsq!x1G>srZXU9gQ?#vJnKP7#UU<(bGr;_J%$~zxD4@tzIr?=1!n^p*k#d&9t?-ub{` z-g4kc-UYys-i5%?-YVc&@43M7-dbRuw;njryBIjx+WynOYMH z*MviEU2btjMPYdz*-lteT@$XYEw8F1H@Q_672(o4#ioagLsKg&8h$}JzP`KyDY$Y` zSH-NLytWQnWOjgcc3^W+;DnvMSXZ$ad{1BeqK2zR$S8n21ABHBn=iLr&NVd z#L>86zKG(SeYQBgytcfqpmW3Mc@34t3(HHzl;Qr09MjR!{Nz)G zw+&oaT~!&btV2zPD{}Hrt*R@Z*D$T7sybX#*N|IQTsc1+$~lZja`IP7Tj5t_#}?0prk%=U3W_!upcBn&Q&Bw%RH66?Ns+ z6%B2*wyhmEx2kdxYRIN`B%99<*PWeTTTni)4%s+D&v12|oc!9thRV{inySk3^M2V{ zd2SP9Qhj+y*fl)0W@34`A|&Q9j;jm_iZK<=WuO*6qdT~uT&5@X0wBVXWMZPm%iu}W8P?1}; zu(}*E$<3+73&WyXk15?itm8&vNmUe!6k{TyY6~rtqQ1g4tg}5asU+X|wfU8G;hM_g ziitJEb&s1~Us+cUN**Uvh6>6n!y>FdNmp1`TvNwdG>Vpx?-H`3aOx=nH8Lq&N0T$t zdA2|Xr=p@+OEt~{X?3y=&kI-ZKx74cwc(g`B7qc}+6I4Nn~fvg($+-!AseX7jpG%9 z6&TJ~nONkkgzM^S6s?Jf6jD&Wu)NN(n@|y6h)K}lc~zxYEVUx!Ca4@NMQm8_NOe?E zs>_cru2q$nKfki72CFY9<{Vq(MW=9ad0lB42j$NzDhpRmsDRbf8vdZn<p5uy{0 zD5i(ctuM#kLkG~{ZXY-;gb6K^Qk5xx>M^bD%xre ztF(xzU4WwUI=6?ZCAC!*;X0SkB5YDHbZAH}uBurO@qks9(#xNxg?1q`(J3NIJ3*Qi zPDcko4{slsUpcR;W??aP|Kf_HTIFE|ttqc2tB8xd@VsKIdTO*q)ONBucw9w!acv}Y zdU#%8@w{+d18p*0dNuKJKBk8&ikGnD3fFCk>8xQ~X({%VVJsH3`nYWDl(DE`j<1U( zY6s+2RX5a>&o6TYqVl>ORnwsKl(NRaJk zT75}Hc`0pqBVo2RC0|_W3eXcF>+8x($}7t2T%i+|giEQ{(}3q4FH34}P`sAsiAWe0 zI7N=IqtLrmj9zEIeC!>fnmQxSbt@}HNc9Ls7SYxk3mO)aN(iO4pk}I~8tBUB@kf3w ze;g}&ksY?3y@a_c^(%2n363*pPlnbXswS!QyzRVe@#O{mpX|kC9_0bu5eD zoLnt!xsI8fV}WgqT>*j)RD`Qf3cE83wX4&ez6@s{*qzXYDOL*X1qB9)mej?ICVZGt zrOAXPr4{wH<%_~oYqVxs)#9)%+n1v{s31JAuCNSEt@(6X8r_O%HQ~H)DK4b3r*+)b zVkdr>t_BxqhpDP+>kil1SG(F1wFNFO(A(=5R?-EAmPpT+7@G4%hL(B!OjuGEuB6?J zJt%2+OB5s07TLLT043DPPGoC}^C5M#<5Eou*Qo*T(y0l|rQo^8#W5w=6Oc>GgWbi| z9-l^}OGy`urM%5?DT`c}%xc3)8#I?e+pB6(P*qj!3RSbaOTqgU>@I6uIz5NmSE;qG zAQeBqvb@fvq-EeRm0tB-MyLohRHZu}wZW<3#V(`DHjGy)S14?GrEsaRoVeHwx^i-H zn&Po+wN-{At;Ph${uBoZ2Da2(DJVizvHmo(}wLIb+aXMcp+j+-$)p?KX&RnNg|DjDB?dOZC zj+CY=tsHH|2V69o^d&coxfS8!8osb&$~`7RsRz@j<*-s)um=e|aj&nzwMG@%5WKKq zVM$emeGI@V`-F{&nyYyxuc$>vaIQ=PT21*8iAJ?uD!S0dS-LZ=+xq{d8m9W;fscWS`YNJG~Hk@Li3m+CGqJe#cka1x<+{Q0hRG8Msl0A@<1BN>wqo z1v(hrJJGc5l%tjSz;x$KI$Gn>aqq$QBb`-TFM#`!6`O5v&N;uFe^n0wd zHdATk(}jl)hoze$B{wZmHG)M1^@6%`-!z!82z!mmI9$fDr5YB_5+*R~;7kOI88;A) zHAtMNK6sm>Ix6J@0h9=RQXAMOx~F5CQB~ud>uOr9v!D(Z5N;pWVptz@xJB-b{f8zr z)SLEsl%(yP;SdUUn%u^!e@zRO@IJPf;yV*rL=~blESicUsvMfVxRzYA8SSO8cJRz* zAFxH%3LY|ugi%aeLz^z5n`lteIy0`8Kk{qs3d}1n#(5@Ik0vNN=xqTL9R4MJ&2-GtNhRrZLKI}{7+my@p+0c+uj!WapBc`e@N*xPzK;c|-I8hEi0Oy_<86veJ zvJrLSM9QHm*R|)W<<%7R*hW_q*}$n7s&Cc`wNNRYo-Jzy4mfDqtQE9pp_OEExB_#l zvT~-;+Q>YrLTGPaUR;6WCzzJI3GGi*ifU?9CplW4u&}z0Pvh01hz^fRW? zbL*=@+tX!a9kmOgrw->>T@us8E)MEBla*thrW#yxyK$jP_6xw~;~ z9k%{83kt*M*3h9o=^K>_U+oQI}cR2 zbx6ZaX@Q@lckqtAqkyp^lcl}acPt$REFGCgs>%_c`6#iNZmcO0o{-xal_gb6aN@x> zPT>?|V;-X8t3nNGtg8D@%=Nt}mP^RsSR?A!Zl6%~yl=oFAQs@4nqVR>$#7TFx%J`V zwm46l94iZvVfP;z)cLjcIngnT?n<|4G@dFtRwR91e@q+a7~SRg80AJy@&=Ti+p`OP zl!x4?vX7m=Bc+W({c=QAu*2-^^J+e(LFZNdj|3V{yhTweC6W>Y0GH?55PKUp*Rr}! z<6{EuKDou!#aKWaF#pqGlP0CMz`HExBH4Kyh@REN&vhsJ&NEYK;5eL$Jicn8=phdmX?DRfF|{K)jDAqgKvIsh5g9biVu zbPyek=%CPfaU_Er1{%Ml#5xF|sm$>$HlHtOf+cn^$p4Lo_OGI@gSGGBF=t4oD4;5l4;VZ#tnuYNbP<)L@JSfA5$f5JmQ#vl0P$8!!| zeaZK7QN@aa8?WpB=!lPRdS}<3&F|gx+RzR?%xhw6MjmRs;lh>=7bjl5Eo$-9KA#Nq zU-zl!VCyG6Hr@64CtrrnEQq^)^8+{bo>S8I{&3%J-@Q4iZcfz+AANbjS${}qSu?6< z+GT;23x7dKSolK=!oqiJgoQ8R2n+8<6c%24DDX%&AccV5bZy~P zdBVb5EQN*F8w!iwqH5uVYr>-UI$A-39t0T#nFOdqVPzBG)sn*MMbMi7@8T5}{h2C@ z{tA=Tm*50~9D;rX{RsvT3?vvtFqmKn!BBz|35F32Cpd{<1i?sxQ3Rt2#t@7p7)LOk zAeSJIU;@EJf=L9E3GxX}COCzlfM5#2sRUCArV*S*FrA=~pa>wn9faxiNpDgGewSL_ zq{a=FH_-Tn$J;Hi+~=XrBw`GFzXCrHp}oht-%5%R1_k2P zP;p2lkP;n~h`H7mDB>Y%M$xdsmk?qwhIpj#K*IR&(jaHT2tp&Bh8D$`1CNxy*#u<- zBQY2Qg9Bx;$N~-U?$9HNv_M%DG>V}FYvCl&cpn*;1*&6FoUsrZ*9OY8U-p(%!6TIm z*&%2K@;GZKvHFB#IN2eC!cQJ1Q&p4&%3>fFkU%NJU{8XOfinYff!R7*jLidOap?RU zMAm-ciI6~9qVT#Y%4Emu7AR9b(I9Png4;e>w0&w~`*f)76AJi5J)!(jgkZ7wJX3pD z^>ikwLRv)=Vp2t5HuVguJI);os8>IVWy`2lKWrK=4U{=~RTPPE$fmX-pa_W)=pdh< zHYsj%pz)E2cpoKZ*sW29-fqiI%OtpU@J6}Jqg+zmv!Lopi>jU+qEgR8ym*dhH(V!< zteKNmQd=!zMXOl46iYBZNJF2hnv@in?PzCnDN%6L9l2{%qOLEhO|79KB%+(RC}q-# zfh28ff49JFP^t-7H{R#2eBx9A9FIgNnj%gjHiZRxBl{s^^)J?T9Z7YzPL)cg#shU) zdt^{6OvWt2t-yk+=^_jgvKz~O8`n}J=@CQ&M2mshm{0=K(xTvHTAHdo>LG#ITo9H} zu2@tLIrQO&tCa#U)Dd@9U^aEQP}|xdiV|2t1ZGikoC{RieH{B?ioKa~-U!i8St9|S zsE4A4c?574xieqUwljmmw-2U`*P#8`B?*Bt%24BK_5#XY=+mD_DPSTpRf9F|;L?$z z8zvZTP7phg+W?%xlBl0R_B&B+Uw+#>piIyNjt%#hwbUw{0Czv8b{y_rtf53O7gHiI zk0cZE>Lk=L(U6@2bv)xbNhP|K>@3khpt7=)OK|DPrcuu$79!}N$ zXmPGiC31&Ax5qq+Aw|=_7OD3oVZga4Dm@;JN25k9GsKo8E#?Af~UjX+y9S@JaVhTV>;ok5XPo>KPs zY4UB{<4(Hu?EgqXdq6_Jk2i!6l)+f_kw+hs1CtdiP#@FON0IuNMIWI+NUC7m3-_WQ>X64-ff-^W3+VFC>X1$P^9B9c zqCa=&&sX&4+Z^!?G>ca3)t{~U^OF>w&mvWWuJa!D%Ge&`$`TuO!azzO$P*z%Jw6+E zU^|<%ly{J;ql5PLf%#THxNAej)V1(Xs9I#${6nhC#t*T{rjJ01y~Pc2=nDyCPP3Tm z)S>twhTQF{b}@rdq-N}CXusFE0mBcmAaLAAI22>a*(cwdNZx3;>mhP)968wN_y&2_ z4&r=ovP)dXmMtn!ZJoC}ejnjL?X)bOd^&@W-MpdWx$^Qx5|sqwOQ(lH!g+tmu7Jsb zAch3Gx-ytdJ(){{p3a}2@TM61+&GkKxex#83M$qnko`!40~E#yWb6yNlmU-oqBft6 zUj)k1cw|AqWJCq{0{>&^`i7IoNP*bR&zC}T45k6R!x)yF02&c-zUF%WI>Ans@`L|; zYyw-Hg_OZ7UtW=b6A(;ZU+5e+9)ziO*!Zn3Q{xZh!!Dp8Vy9`@+7!1o&DQpIN9;?) z&Q^aS_;1(LV3*Tjw3k}$!xb4f15QQ!(OgD~Lp>GMLzNE)=vETF>0nx78j1$#+r+dO zR{hY;)-9*&O`N#|mRlUOJS7qY8>Q<_Iwkfck^L_3-z|-fj}UdR^1pmoA|EZOQz+;# zo8jEaZwqjNt4ei71kuzY>ci-!Wgi_rQQMjR#56xUSZ7D^;iA`%QhZI_j_XHP69`Tv zaB=NJW37gW10=lPi7tp`Z-Fp6?dpwr!UsyKG%}w}P)0D)1Hd&c;e9@eNmDaIZ~?1X z;|>m0xQBw4vSKykR`~SW5kstc0Z7A~d_LlMxtIBNhetx*VEdPBCzH7V=*Ar3YsN3A zASxNIRmvKKG@pTYUC~c>I+`dllV{cxSD#u{i3iDWM*>gs@%@2X19p67Ax7b==Q*cN zD9XiG&c;<&_dQ)bQ#)$W@SOf|;P2>sA1SZAwz{IYfu4r&QH&rgIvFBXKc_+8hO+qk z9D=lXgqR<2#5um!rWvC1QEv4Jk&5@ROers|sj97-R~JNPs;g?$Ly=;TA#$F-FL=bz zoZz^MiXh*XtPSF3ARZaP_m8p3Fho4EmoseOkeoq7hYY~c6S-m4@x{gA;r-_o_aEMW z*wEtPCE@M-Ox0hF75-?sSP9ypO~m7kN%2KNM3qyRaaCeEGM=!wxTcaG1nGBv|7AD;L%$kkh(UBGiM~%A#7$Q& z!mldh+lFX-t*8uTDUCdc6f7&Q4VHw%mBCOLPs`%rX*6znaRGMg~Fsa9BBf)Kw9q zrvg!*;UIs(30g{HaaDaq2zd+ER)H18w|$ z#$Z)VunKN#g2nv(HqeE|4dfCJRV-Y{-MP*XRrG8J_Z`(M%1g-uzWnja)rz80c8V)G zpJ;xn2V}H-9v4Tu(cOo;TzM&Se7+$TIXyN~do<2;U#)k2l~Q%s5;RUd+J&k~KdOR6 zDcd|9@Ru%wB@G-NaoN7WjoZ-=wj8y{C<}FUs&;yGnx4>YK+EC5B77x2CzxN?OEo;k zXl*&>C2A9Vvm4Ja)rD2&iWlJn*OAx`== z9h!TdZUkoxqZu$$U(ttV^k|$a^Vd#D#pldov>}H7pZ2~3Dyk)2mu{L2lB49H1cBX6 zY>*rzXHb%WpMGyTPkvPoPcL1Z9GcyD0CV83P9 zP(T7dA|QsH;0$cMD-S!tTM^sTH_USrn3H{sUu+0I^Cno*3aLD1OriFofdA*svV!gOzW@6+E zVm0o?OlDHoHZYp)&NXgsyohA_G*aVZ`GKyADtyU9w<7VH-8qMBD*r~ego%R_j;!t* z8QQ$T`yo3o!RcQ%RoKk^%f8bjxx*jOZ3b{i-OEcKoERhU4s-+v=`04nI0awp0w9h* zC$lrE|6nsfHIO^d-jRRIkrv<);K*y6s{R$1EnS5lx~=;sAT`Exz8=>UGz!50Bybsz`; z@wfv`LqKx@&{O6D^pC+8KLH%d3_NuJu7TjW!-lppUts%r)uFOLN(hjrAmGa#xV!!) zrl1W!5YX58^CJl~9)UUl7yD89c22QX67jK~JQ*R?)e_+x8 z`qBJAI^gYf@Noy>;9L;^A57cv_-*7h1k?;rHv@damp<_82*UXS-@c%9L7-#7sRn@9 z;QP)}9oop#R&A(nqy>j{2Yz8qZChpkXY;rp{NoV#S3wX1T+0^V8?aplIFDFRJ_vke zfi_bq@GC(e4Svz!1NRAb5EgD3xK0AVuLFqXR|)^Qyodo!Q{X*B5N{+%2mC7-h|_f= zMz~J=K;6Tobp=|Bf`Fe2P*?x``V#}bRW@Sh-VsY6aQ)e8aPE(S^qfG-1O)aQQ%h>DoZK28haOo&1vhzKAX zVsa5;1PXzZK_XCi14s{I-r+(Gkr5&Y@Tejr_(wYQpTeTJq1?>XskF~5^A8VSx)cG$ zONd{Y6*`m2v+~I?+4@fUAv}(18Nv|0o1*CSaspCDkgpG#2HFKXlF*ZY6a3!b zHC;3(#16ZV)Bm1fKnp^9;OB(&T-&lfU?707XA0iFhGiEcH5G0QghoT0%v2ajG#ZUX zW2GP|Su0=#+yWZ2X@wmBCHbR*D1PP-1@jLO4pFqTUO-b3;tfa1QzWC&fY0$eR?jmPpmwGFAH##^Y&@=at5PpA+M_b1cp0}Cv zHlfw&M{FC*m2aWnpZ4tMPze{VG5r{^Tchos(b378?|t@Iqv_ZWBM)Lt$71{sHVsV4 zSWvJ=>g>K+z(7(%m+wtI@xoXt8YM2DC3+JbyNgcW4p>QQBq*&{=;v3JetB3o99e}r1!nbJRNCXYg5kR zo+eYG=8aC4C}Wg65Le(hzwR0mdFWiit!3>2D^-${{PNO9!|#^U`;_JyiC2@5t!EXT z(uD8ddPFg~{Qa(d6lmXYCY_Ab92j7Vp-*N9VtxgS7r9XobR%zKNd+hDr< zJl%c$0>vEzyu?ACz`y~+28Na$P_Z(vO~x?jenQ` zI3gYR50VIiDB=2|M7;a$c=qgTf7G*G7+dh~+bv}<Sfa=Cu0C7k{Aso?>pQuCbuq$O~V>v*$v$IeD zu!)NQm(1oE2!e+sA-&MA5e&azz^w-QH((H9c9UBr7*Kma6I#eN?f~?Wfn+g|6c)n5 zxFfw`MFSEWYxQr(MI4fMXYFl35|SD~5|SJVkGaZ)ZviyyV%3j*F5zhOC0jmTKEzX? zx?9+D#>(i@Rbp8-ly*x*HzntUjOR_d#}o^)UD?ESeb`3`2DIvM0#%69sqi#=ey`em z?F%z*hleI}O&iGfb=Qtnh}49V)sALZ-nM5WnsGV$7Gtu9PW(+JsnPvL^((fId&CHX zE8Ug`yqDx`3YnHQTc%{4s(hTJj+f#csYQpC&wqXSjD+fuO-PBh&>N}_JpHkbG==%^ zFGZ|qxb!Uqi$VjY=;W?wAAUSHrXRk5hzc%LVFcR=${lX>x_-w?-J6Ll^xZe&KCm>ZeXeWNW@MlV=e z7UmlH`t*_Gb`9b&qX#e8bRH8FT%7an7EIoktSn=A<9V!lLJyf<@52%`@u2iCK3|V> zAKFK6YoDH@Xebf$q%NNMS-!z?MqQgG-YaWpY9#&)#TF#a0Z5#IXo$%+7nY0&6j=Ka z?7+a^f)VR>00tzAl9L=zFu_37R3lK?SQ&9pqu^-bAnNV8p9mrZ5by_>3lNnw*9fR)T`AQ2e1VAO)a zx(#-Akc1ruvjKL8x4;d6oe5+F8En}B*s1;%*oixCA>CiZ9|T1ez?tVpMP))!X;4(^ zb|r}sLQ&C>(pGXv1e3&HB?p5yc&RN=+z;$GfVUVN{Q||@g1jN+?Ff*NBqxTOV+R*w zx4}($0Vc)~$oqCc`UmfGZSX%^Oqm!r$IdAMOsE^^>-sUSBx}kngiUPtagZx-E=30I zbH{Vpsu^KJA(SaMobAN+DSqt^81#-_>ri@2ep|ltK*hx+_i@KgUa68yTj!XRutZHG z^T(9uP7bl@vM=pdO*DB>x9a&)kwi==_qD?Al84P4$I`GbW}N!f6^@7UEzyUSrUXSN zEf4HLY6^FqrfDs%Afn8jbNl8dmW~%z681c#?Z`#u?qije@hW=xdh(K{$g>~v589>X zeFSQzYxmAQm{{6Xn=P1`rN2vo;v;Fo2v;|T<;6m;=soNFCT%SFP4cdr)iu)%W8;kR z#u|rYG5&kmBI=j-etou2j@vz}!7{aVbJrP!Xp@ukS)}y;1+u^W+G2 zU&b(vvZK?=!p;}FZB35Eb#pjMXU0uFS^D~ssW4~n^SdRP4?fsBs=Tx&xo}F6_!#jZ zaeXisLx+Py^TN}cyHOpJDz|nCet70AKAZWu&?e(C!APNIyX8`537M`IO?D*L0|KGm z`rHyFjbofrH;0Of3PMA9ziFj&RenFf7x^jwYp3TG-OLy7gO9V#zLUugVbNV5Y2m$an7YwLhRal-}9N$zX5CRE5Dj3j@Q>$b-Cv$7wMX0_)FTc1B42S3ne+7sZ{MdElN{ zg-v;Ksu$svejh9QzBkj1>Y6Id+LR}k6F+f5syl*Hq9q{=o*|O%Zz;|~wn&!0H>P(E zJAlL}93nEnmccmO;_cw6R)|(uJV(~CWIs0j!a%yU5~<}i?bipGQA!-vEVk>?d1;=5 zJ?iQNdD6@k?W{KHp|QSFJTWq#w! zh0{28ou0)H=96zqzT-!Al+z0*?>N!U26Q=S@Yj__t{nTSXF-*(ZkZORrxCv3WVb$I zMS@k45J>-^8XpiKkSiYKV<#S=?B_jZIw^u0ZsC)xMVnpopU2y>}s{1nAq^)Jo-=63cZz41y#M8Omh#9|YZ-}RQBi=p;J%^9ag z#LKEKYvwMVW)_waAT7Lg_SONr=jEc_o%>rtPRnUGf6A|wxL#)~#q2C3B;GUiIXKROLLK8bOpxAWsu z(;-g6T68?5%0y+Nsd+9czFuG65rqYtv}4DoPwje8h-0pXaLg6kjY~*~K;xLRfm`}^ z4}s|z<~ZO4R!oF{OUK|4R>Txry%3z^#p4j#plvk(q^f|?v>A1bB{?DDH+;-PkEt_p zi|2)B+sqioMAAir&7F^GF0L^-O-sLgDTppY61$nQrMok;&|@Hs6eNR|1)Kj85)xw4 zShN*{ibNuo@lj*NQ6o^)@V{hP{7(wDdm#aM6-`0U=>rtk0S)HdW~Qdx8m0zvQc`FY zQAkPy5>>}a%Af@xKG-86J^Qcawi(cJEovI%;O)n~d3Xhnqx}v913@4d(EOsKkcdGI z%Pg8Zs)m5%CO{%~jx1P0IQcKBi9Hbi&m437HpiP}6!epff@A;#B`bl(V1ANO5C*ep z{h!%TK&1Fi&Ede`iaA11|V4$T^C8QZXr7O%*Jxp*Zcd!y*2(-u$O?8Go5XOcOkp1?Pv5DYi8LJMZ`ZPa_Bspak(l9vk;UeECLDCy z-8>wVNMOy3U5U`U>TVG8^}X0M-}r96@k^5noHZOz6h(_KoamvgdXavtyf9Fgg2d-* zqTI!P8nMK*f`B?>vL@flX_p5+*)8D}J$LO9Ymfb7A)ZOG zucb)JPI1S?y5Dig4hX7rlW;q=$m|t={eJrA9^sS_?p5j{EeUr_M``U+-MPRRUW{aP zsh+%0QAbMQ9SJujC5M#iDYK(G){W*%(+Hbq#Dn;Q??-PNx2d4oipQ>ZDAJdkU7e;$^FA)YBnV4q|<6 z-F*>}U41w+=S$_Np0@<&4zMzJP=3R4?#j(_eEp?S1nc?f{X+SSxQ#NFxorH7HouYO z$*aOc4$dnz)C#q=hZd$ri*8ZGQ+i3|Jc`hN82{~?uD~Z=F}v|E)N}k?M^VTJhPRsS zBdAK54KCWKb{*IfHMGIJsIfz^s0*mI&M11j|7&`!Pin30i~bODRW632*LH;Xse~^% zk=j(dD)nLC`5=j-JVs83Lh8u#33%>Y4bFACrkyvq@?oZU74uChZ=9mch0O0Z(LUW{ z22WmyUhGJWs_fJ>6MK|tw7~SEe?_}*szA^3=<{Th(r9DAiEkv;U*=icSSlK*?|#VP z&5lm5JE`kG(|Ry{0vg9OV>_=3?}@p;5XGsJCQWblfSH7uPViQO2ltnQcBf*-7&G_; zrcIN-oy7J!guI@2n0bD5_mwEc;%+vPMlO2 zw6tcAXUBWTWv46Hu$+_P=+e`1VQ6U1J?*Md>FRoxPsPxBPlfhKjx5C@!pScuxNV&HIre*J8VBmNg%r zW@ZM3gOLi4ok*q@smf}f<^^1ru)RGWJmSZwh;r&HrxXdM+vokAcFd{?yPsJESIa3H zfASLh+yZ@-VRu-{;{1HhD{4HGk6mb5Xp~mt9`#WFcVl}gTaMdQcz@Q`P)^KYeV+Fw z;)-(QNX@vZx)LeVcQ5{`C(+UMGfO-l@h@^Cm3?MtX?&IoT+bCweG1ogLq~XCdh5Zm zE5w6<{){8#m*FA0M9upcE`XK(04ptjVI=`$?;Fp2WUhAh2AjtCr(W})!>0WNPC%SV zKxkPE1|tQ4Qv&*Fm4L1PW?2760Qc&7sotBW03M2vqJdHVkoH}VaKoz~E|8E6Rd^W$ z_uZB{_!$x5r8D)S{hA`Py~MhW;VqXh>8AcxFT!=K)~-ss#%l6WHYd^FKZdzZ!}8WX z%)=yF{YvRsO-~YrrQV~T;*6f&Q1A1%F-FVa$z^?#=0>R>?=d}zbwGypTv`oqujQz$ z`Yhccwa4j6mV67_35n`R>+mx)?0F_@m){-{cSuNoX3;!{euQ7Q!KzuyRF0y6(Qn>W2wBBbCpK?0mx1B_U7Y74N^<{$6 z*d=k)3p5|E9AKF%dz*byNpHONaM#kK$AJoJ1~(+IdS{Vss%~O5?_!KKUktW;uvj@D z0&lw(9Uc`tihLo#5-?iEz0mcIj(pV7m$F~lDbX5V8qH&C2L+|3r9Hl8v_g^Kup~_^ z#S(L1{ORt&)rN=cy`GEwVY{HmH!6Vm*2gW5o zToGt~>*x6ax<4v>-`Ox+Rx~)^biAJUe5L|gq8U5deF6SMqnS z!NN2a-VZY_u5zaO6Im^nIgdY<7Q0Y^vmhX>BkD+a>ps;&_Tj{&hi`|_fX`zktzg6$ z%QJ5r6Zn~_deinSH^d;UZ$g?wN=;)qY<3e>WH&e ztf`(<1;v6-U0DVBTV!=}e7^ITd3tEj9;jHnQq%V~;jm%e&;z}r%?HiM>fI6Q;U^1$ zYQi=0)>#!71^3l~8!Fo2;paOBzRJJ4!r0k#Pq74T9Mjf`yn!n^!OX#!kmR@arul&& zTLMM0^ph67mE3jm8Og8wQm!IXR99c0$sT#m`)*;cs)9h;>!2WOkHLJ!xo|vMBCKs- z=0vBgpZZ|RVqc})X*tp(5o3Oirus=AaNZ?EG&lT-J!qOZ%q0#r7W%n-EW{C70QpA| z=FV^kMB)EndAx{^92Am-m`G$7A(D-dkzjPL#h>dVm#yS3J`GkkALMGKJsTdd+)kT%rL_!XlKLN9(4m8&~<#3Vn7LUeJj?1f&^X0qh3DQNQD z3roBo@0`tFS1TO~OQ~W{U2E^^jQFN%S>?=I*EXigAmT0;YjHTWo_f@hpM_J=(`PQw ze@}sDcb{-bEBB}J0P6(6$eQ7(}|4H$5%>=o0~L)=|r=m z#(QVfhXZJ5`D#Rs{rNJc|1=^{IPmY;g^z}N**>llDa1Z24)LK}N_tXYA2E&VyJ3K} z9my$5MI4!azaQsp%vfJv#5o=>I(BE+UcU8(?0RYW4~DbT9??&!B%eI4(IMwqnZvlc zJrR|lSdS3Ek_Zh_GAA>g;pVC|P7IeCdsjQ48XB)u8%>x>9C%O1qG2tdw0_|pN%2tC z2xF7LLuE>lNL12I1!oeR>Q(Ew9ws`uAgWc4da+XrE)pV>h4?<)gqr;_%bD-cL6gx5 zU-v2U#0h5pSERcg?jGVEbUmp);(h0Urfq|dzP7pcu{W&M>xuM!TK&>ePulznOX<}w zdfX_m;^!EvrgUR)r5t+RUda_l%AoO0Nuc)VqmE7Nq(q z>#`m#T_oRo<7aFoWQhr`^or4Gem3_9+FvvK3U_vl>S+lfvN{>Cdgk^ea^|h<3YXq7 zU%EkGZnW?=k7sOuLl0@+#LcJPo>crq{BQD3oDw29C;f4TKEH@<;T>Hsl7Z{mq$C+- zy@I2s$EVm*PG0Tk!d>SJIT?hb-#d&Dbkq!7A!*#7cuBpb`y*jM)rACCjyFD4^z%w8 zQ6=|0pEG%JWRyQPqzsCTzk9f1e???va}+_+^>@3XTN)&9My0Dqu{a33ygTOQFSj=d e*C{o|fhWI>U3uy8FpQjiO0mB5pw8sgLOQ~fz(oa+Vo%^pe6c;YPqOf`?X>)~TUc1c|I`r(-Kc(O8eQ&z`hFep*-q5E{ z-vKE%c2Bu|V4sv;eNtLpdPT~ueYhCb%?C&h-&7K!+Lp<`QjK=;7|@Oi)r-7ab?;&y*hl0=D3b-9SJoPYR37DDiRB! z3b)9Wb1Na2$NC?9Wv$tD|F6rL1K{e=rj)|AO*!>}!;h6*$6GD|$4iw{Nv;FJ=!tvd z33La>It`;-k79K!t*qsyuFM&vPtZJ8w0TI0X+_+W=|RCdZUAwWU8oIRMCZJ zb1-$I`+G8~Fh0dFdz~&Zy2k97Hl&c*f%%+=4lG3r3#_4H`A1|_JI|8{I1CfZ zvBq7davOli;ojU|E3HaYwG@+sy*TMzdQrk)@i+LItwX8-pd_p z2stMI<&8cO-iZvH$iRsVoX9{<88CafoXSt|EE&jp(Uhe)j~{Ug<*TL>>CfAptURXm z|3|fdWSm|f$zNw!goI*#faO>vLMUDFA}|Fapun9B90rBlA3nB3&d>3F1s z+$sA1J(p(Qnd85;J+v;Fwc}Vv353IPVl$#%;mk)k%XAmc`Mi)GCE#;TN519f=;!1o z-W^F(Jw;>whqw)nn@gi7$G9pM=IY35oe)lB;6w)Ul7YO;xxDxhojfcWSr?No^b8Po znT~m{@r34y44lZoi42^`z=;f;$iV-;4EziG{o~k-WZj$`>vNu7NkSZarIgpB?L-(% z%%}3x?kG=37(&%ty_pv`OtseS3E}@v2GIGrj^XT?$lHX2-<<9q7`{#4Wytl2`t#t5 zT|$*F6msSQ+CZPjC-RNcDZuX)SR5> ziDwBrmyfwLgKIJ^Hb?6UhC2(&k?po2+CyLd&SI2)*3yEZN$RLpWATb1Fww zITblP*FJ@wX?1>m$(z$L)7iq@9D{u(F0FSG2SB>nvl5?X(Q}#+cXVY@=gP9Pypwo= z#9&ok#gT--PMc{(-x2jVzU#XpzsQ^nj(gBcmH;S&+c_^y~aE5$vEgS_gw)t!(}WZ*;wPGsOj22Nz)Ly;8@4JS8*JgIaMyMc;Mk1drH^yRDS)< zJ-hSao)n&$?X0Pkhwn$CuO9M!m5+*fQ=92l>))~E+7Qs(Q^nE0ddw6^P7Te8A4e{; zn3!2>jwp7-EXv0!%%W}_nPZk>`!T219gCZA*nf_{jFzpA`@gCF`lYzX*a)#kR)~M| z#}>yGoCG+ctD1+VLY?Z4Q@=*W2P>LiQi&-tN9`-`;~oaEHD5xI>rra7zfFUUIakfU z;Q$0)lK&S48^%8_pk2muBm82RPxxt%%PC zzm&{!#~coh#9_{E*i^yxx&8&LR11Bs#T>H)6R*P9i3-X&w}aK{l*-j0*sQy7|x#*Ae5LZD( z+o>q`@cwRTPKn8d=d{ns6tIcI969c+AMhFVoL?5LN~oFb-bz#O`1e+L(3tBmBFk8s3+W_HZgIVh7#X6}4~n8~+OWCl{!aub6wm2iyVv5qDzvuQJV z2Zw2{gAe7Ivd6B>FC67RSpjnXZ58t_#MYqfHctGu5P_Pnoi^z}8AKjQ5GA^-_v(%@ zo{p6pE|(abS_Nc_G8@RjQ1Eg8&Y>FDU$$6nA+0DwYMB)!)}v4`AF|}smK@hybs}*@ z8;&cOa&xq0*+AwDpg4yKj}kK_j1Qxnb)4jMPuHUc&w1KTAR;yycm%6=v0%nD5uYk2}gE}F_$+txoEL} z)Rg5vU9C$qMH1^XQ9%u{yC*qSXE;oZh-3Csno}6}&%;ojiAxAhdB-?tlf_}&kC~#4t8v`WNyBmba1D_|&V@Om?B6XK z|K=#+IG>IwN|2d`9T2A`%P)h?F-vjmqfszN-GvAx{$A7!Am|@6Tqu}W&x!w*trudyNixKoUr**f%&EHAN|KXCuD)|J$_tN;pw;??ft-%n zRvN^VnPmfU1NgT!B};8NM}v|aMtBw%_G8YzpkfM--HX;=-i2Jp%`x4W(6T7T!(e70 zLp#d}n@Q|fJo_wF9LL8v`Qz53ww&kHG3%iL?!*f*)2YxQO=n6_~mw}beR9h zky9bH>in3PBtw~hyW!P9G6f_Kgmd z!Og>9<20w*9T!<@F{3sM{s%vz?xbp2wv%w5b4dSS-TwhsoIul=lJ+#`dYKN$) zE*BrU6d7OUD5{x#X8x3mGq)(SLtFBX`EndNqOIqc0^*CiOdZn@3jbu=#9aLH zuH$X(Kjtgfh_mOENoOt* zz~()5Rid^x6CfFxX}*=FU3IUSpq=iqJ?mA`m?uLL89g4&e z@3*bQxQV9}V_DQq4LRg6POq6Ph@%|rve)ttlB#}*cdIX=KVX2H)`U@s z>V@_Dw3$CyZ7Wzaoqqc{u8`*>P&dT&kR3{o36J$&Ov-MN7xFg)h*ckxGP%g~A$tH4 zZp;=^Zb-^9|F$NXaSkch-4*M#czUi9yT>op{ED{CJ>}6?nW%#J|Ddz#)~kDFy)#cg zhe)J)jnNQ8$}*YmkP1vnA!egfg~_81slkNuVRY&+xym7@GwFt08J+q}S~xME z!(l*bee+P!lYYj%+Ct;Gxr=J)H^Gg4A=cdo>#L~p|TP)d0wxQ;5k{xlL4!@*=k`^ z$aN8QmYq8(>TTBfwX0ZV1S#rWpCarq-uS7c?hc4N_kfAHk{Sl0 z)JJxHRZ=4YYJ0x*V%5~Iq(-9`i>SG}zsdxa)EFiu?uq$%Frb(l{}HQeJbRvC=V@j2 zUP!HYu!QtrMo?MpVA2y)MS8G`IyItrW;GC!?A1_@M`YFhZPTtgAfz*JtI;`K9R(?( z9>q#Se$xGOK*;LD?Z$H>RV0Dh^$u1{(P^sQNT7C|bG~>^Q%%)@1hRViLSxlbeZxBa zb)ODU7pXb<$g0V?Vl^bVNIjpAJimF+SY4!E%tzUne9&a$B6Tny#awlZkYw*7RVtDE ztkEMg-M^FwbOsC;oqm3MRhr2#tjiQLk@9RczM1jdUR8ol5!Ks{`Sz+I>%6Di)ectY zGO03F{7hBt)%i@i+OgVRHBF>e{P3LUq{FHu*S_DbW9`)?OiJ56ZLcn8a-Chr+N)G1 z57||#y}E`;KD%nQSJyMCZdcm&>P99HIHU)Y1iR93=zL`Ib32nu9Wsc? zwRY`oukK=Ut`qaUOorOEx4jz1WUteTkxW`Ttr*LspA+-=MC$ibPKxF55!RW3vnfTG z?6p_R6KRYM#mSLKMsOtv_4{Z$=2O*5*4f(D=yU?%v9U@>vUi=@%sOY*6VDkzS55Ao zV@P*(RCJmc(nGzQG!wl%xU~?aZc=-h_`Ah3kx!W{X%*Ai$E1v7wV%nEh?`nLRtK2W z*K>!+mrT@{nCIV_^mhClVS>qR{TyZT)!i|xKbTx_K}_e*B+6m41hbOdr2a~xSM@@Y^j7B= zAgdmFY-9v|)dnWN+q2&7>f(YlyZ6}nH9&p9WRqR72dJF|DZ)Q(gagzb*12D|qMkQE zeO8eA>lD2{V|7sbnIvFOCYc|g4ltQ+b2vbK&7`tEGm_P}OnTWI4p84QdEK%4o=FEs z=OB~k9GycP;acrE-5;QSVp2Cz;`uZjp#DU=lzLh3;*!yds1UVu-AK`ys_q7%)uxm_ z%cT2vtHeS>p|fDM=nyH$a_!_^z8 zU8zUsil1a}xZ1=zpEQy&*2N#A$)(n6jM~OJWA!@U#UH13a9!iuh@XsLoZ8LpYHQDf z52zr2xvn0%_B8d7`a^1WtP%=S+uw9Z{=$^8 zU-djq_a9P)3sW|#>;B3>T_>~EcHN3H!30%?$uymLr6#BpCbw(Pp+7;@C`|p8G)!7L z6*^}y8HBk`qb?mf=dx8z8_xvQk;#>MeoIS`LCVLZ9 zFSeScJ&*98AOfAvc9-&mx|MaR*m3xTx{b-ZHo_;=9h{9{aTz2l?GtJ&>vXX*Zi<@7 zI#1j2GetelWSXukBY09h$5yTMnYyw!U9I7G&b4Ri>1qR8U3rnIYr5LVma%>D zvTDWTIy>rKRu?jv|Gaok_g_|RiqNdwFhb@T^N5 zzgFGDR&B86NT04%qgdzVuBH{c)TfB2h;n?qf1VpB+eN9Ft=DoI>OYCTHpM4w1P` zCOc#~lauvshjcccOxdV$i_|qbX_wl`I)CYPV@U9snpuqc;_LH7C)xWEsRVFyB<`<)u4z*WXht&cmk2>UK_Or*HD-Wv`OxD_SO5gkO6smohjVJVmlpu1OopGU;R)X4fqn&Y~H@pP-`AYX- zMi6?Vm{hW}AoQj%Ime#6BX2g7Zo0oRf_&a$CX;N0`MuYf6t?Xu>}_Ilnr(YA?*k@} zSW?paj7cHgU%3AAzAr)j^?eV?M!H|!JAzhVUB&$vnLE|Jzu2mDJ(IEOUL}+kQN473 zkrfE6zSDbJBDGnk)BWP7a#-CvgUNh{oXMoEL(YO9+!?a-w7OTHb+%cZ`kK7vkOr(X z!Xalf>En=wOeQ>C9ud?9FBJvfX2M1>xS&5vKb$X`Pw+nlahC z$y>~|FTlCD?OfWc_txa1iiY&@)^qK@p}$Bc-S6jZIdvv_@MNr_L~z~a?cv%#!Bqf} zWUrsM7X)`R-ZoYPG%2aAu(R^MJas5^HsWrGcpl*Wz*cp%PE&sn6Qvq$5kLJv{@`|< zc99U$@si0;eXMNaXRuc~na0?hv7TBr$3x76y|T%au~du=BFWxhuL|o-c?YXM>)ZoE zeoloSLngCMah$1%Jni4(O<__L>yOcyB?R6LS zq9oOI60U&6Y8nVxJ%E*jNVjmB*OAF{SdoZS_NICFmZXu{Pv<8inC1;*GE!S*1T(zJ zC1=9VR*a3|lToR9%A3pNPsoPMXRCU!B9ak2UmVXDvY!(3VmZ8&bbh^ZscVJzAY0wv%~-AQ9%h|W_ZX{{MBry+Ga+SymEIiI z**M1dS?R4~KM(4sqbJ+2f4WYVS?fxfyp;rR}S|jZEr~kC9DG&UAF% z;&^UtV&YltZRTujL$(Uy^anUBNbNgT+$=)XK1GYL;*<+n#Zwm-n_0U#R)ehDvq8j1Km;9tfJG@Y#R^S<Ps)7%utYq%|$2O z|I$k=L*uX&R({gSfS)8-;W-Y@J4BMbuf3v7UbN&p5c2cqv&PR)np}jtPh^$u|Kye6 z+SmOkWM{%pUP-nZg0>r-GOW`9eM)4i`pGNDWDv&27noy2Qn>bWah4&H?EU0bWS!2J zi%z=#ncHEVepoJ^$u4U~HP))wb( z@`Gn3e!DU=(XI+xl=`a@0#lxU36q7RJxeZSatF>JMyCst;U!|^1|}mfh>;t)_ES(h zSxtrKu1r1|79-u59JnV&x--debb2tU9=RgNf8)t|Pp1Jrhu;**q zVqU+lR8#*XBG8%H8&7oj%y^Q&kjYW3KSD||DWYbjiOy729)w2J(KbSMCY1LVvFFt| zgOF93AjN-`BW(YGu}bmRa)i&}ik@_^!}r&7T_6NF~&JvgTj$p~ut-?N{aFq_0rZU1++>Rdr0O!sU1N4OOYaYkdE zLbZLbEVb)0+pgMv;j*-D47Psi_@$XN#!L~<_55-mN;U5;B-yLymoH24T>5~gZv2?y zxk{6fb8rpDq@!Pfb?(AxNyycHB_<7b8>?&lDqPp1(YOPNF+Wvx@~bg<%puje_R3h1 zNGI9rgK6-SX+~!b3T*jiujt`3_^MCzFu_F{l2VIMBRj$ zLRM2%U%wf9E`{-9NOLBGadxQ1W23L%iZeC{`vfAmlk8u>I@b>sopy=0`R$n0K*q%C zHveKKEgYRo*-vBZ=Qh7RlS;^sc<%3aWO9ww8R&Om(!%Q8;rC=x#p>MU_v4Jcyife3 z`$POYm~=Q0BZHZAsw=G^ojaMF>FC_WB)MTs=WZsWFq$YrvKqpqg=2LOw=@}NPigxQ z|6V4iU^O-*jmb$iKSTVXOpai_8=YZHS~)tyL6o|5zSN%V4e>{E?Pp;mh}FGBFn%7v z3Tem~CW|de^B-iZp}H@osx*H*>r})zB&&2k&3}lqI;ORdj3CW_g2|1DfOIl~Vg3{* zhpf&Be;SjWR%fI?ok{)+jMZp=29sZ`&RBmYkIbJi>LliI{w$7XwI==Yjq{&o^0A}y zER#|X#;oSD=S{;TKe*TJKgT*1tj>6U1(Q9tcgFiGncR+v2;XOt=(K%1$K2*GGv>$p zA26xC%Oy1Go^kU00moCYkhGLqv4cI|W5?lm|0DJ@shvca?vM9BW}U9OrR@@lP=5Am z9rCjiI!ZOczK+_S?2Y$#GkFK+0wIquDWXot8BOvtS?e^!)w<|B=6}j{eP!D|OOw{N z?X&#PSZBP=*i-%&Ovc!jKJD*gQq-=H&-nY9_%`#;_+RqKyuz+g&-h<4X>8ZA+5RE6 zx(fS4iFuy?JCiGHUGw}SOcpshe=^x)>ze1Qa?}^8cC60xJtjXoe*AJnq4UNqCiCXy5Zo~3?E)~Vq5=?kLNd)On%h+69RXPs%b zr7J-w!ekraN`C;8A8mxM`-7Q$W>?YG{*ZDs=4;&Tsaa?#_Kp5MoFA{TkTSt)|2`&X zHj9zrT>HNJ@FohZrow6r>s))U=%o9r{Rdg6ruLlfuk)W~o!hYUBr77%vCb~6Y~p8~ z{|bmwo0CN+*<0tYWStKX&$2qS^LztjC}J*A%x9ABzu~W9ojREBq%&2$;TI}TBXc>% zp&><>JcF@n$jKl|t-M)CviF8xoOQZmjET-hO@2V04JpAo=b(2CNoLX;tuUk%lWWk+ zhLmCQgrifANm-mvjZO-arH)P&CNE=z8=Y!QYC7@MV6tyWOs5u;?N`u}cbs#@ z&v$+wZbemG>lrerJgqj@;Ld>|!&s+TQz1>g@BC-W(@fcPP)J7boj;Gs0ZV@HU*=XE zwmo>rU&Rq#K2xlcy+i&Qj&PukaCFkIAe7ZXx0`r=^*>}Q?|EbOtN$sJmbhx82$Q{| zAk@;T7YZ4jR3s3c(zdQ5L6kBRIt%U=9i>WvP!2cTD0L-!rGms1YFFv!gk%KeK`0yV z+iX+#yU?=6O!yz3eI8D!Dgcl2w8RQBejoCYE3#I+i-m$ep&~e z+3Fc<)jH_GR_B>`k~)I$c($XZV{iwP@CEUl?sp9CW2@B{8*+6pls(rUZd%bPSjbji zn{nuO3SMH8g7+yUo=(9Ew%UYyUqo3PO24Vn^>- zO@6~225HyWpil+M;T?LklnKTLMM0G6ZZkhNNMAU(KA>a+|Ci!#A@^SqqHI|4(_io6FRRXDJDuS4rVjK_q1Cwm&u!$BVat5lR{1_$}qXi@l#Ix zIHWC;_nq1=6CH6TChnqR$3tUmU`3h*eX)i=U7YT4E)MQtGG|nbOshyUuGu$=bt)HF z60BlBk5}}y)skQj>+HhJB|ksqUlN>*yN2+5t`G&QvzUB&UW_zkvix~d*OH)dC5o^c zu6HD!CBem&XznB}7M)~oN$@h0!I*KRlk6>Lf}I0qlhnS62x{++Rf32ruqoKaR-ZNz z&v+IP>|~vvvBMXgcZi^_4=@MC^E<(QCKuddtUe+F&o|p_>}4H1cd)J48Z?FA==RWEcM8 zK(g^O2(_!&x8k{}{~PImwASONVS(R+{Y-As$>p3lVmBAuNHCxt&&p^PP55!3mLb*kLpsnO_fvhu5X@Ns@my!8u`>=g-5 zXVM*KY9Xh9ke@CXhhkNdbkMHHx(Io=K*_KXtk64Fr&QRK{d|T|Cw@wW!`Z49#*g@^ ztVtEjCL#k=61pM zVVG2n`r=I;bH@UWi6G|TID3j!<1mH&obTvVU_Uq8c+LwOvrc)P!;S@7YEsNvwF*1H zs)(wM{h@en9roqA%Hi3lAw$@YZ>?H~!<55@eXvF8VE%$>uLtdrm7xpO!bI!aBpt>_Xi zVV#4W#ZSiqU5P;Fs}zYj+3OZ=Vb6~bSLzk)C8nz3Ak^=*RtZV>hlg8P=Zv(N&Tp(U zXQ;;u^NFVUUMj8W5e^9Y{6B(Ax)T+c63@WNgox{Y02bt#B8iuG1-dz7;*s!-u%V< zB9e^S+px}G$e8F%(PR|zW5|`P^8)(Rkd91-;;Dxr*D@(>_yq1tJ(NrD65e zK6+}lh;@pn{+I=ZJjYfIFbjwzdyB$_Y<2cuo*ISKA8+##!MrGce~j#_L1(mc@vN3~ zGJ<8{mu&T6Yo)DLgx}YgiP|^fI*)XeS`n71N#yYnF_OY$Am+Q#sldd?Tb+i~X3|7^ zCO@Y!`RT!!PCX_+IG)dBGW_nCPJJf%bi2q;118lS&&`<3b4c@=l*7$F{yKFZ?L}9F zEt#x$NOvY*W8Z6Z?q@#(Kn|UXrlYR;OfE$t3|YV=1!sIi7BYDmo();VIiI$zeLQC9}DV9an@$5w{lv7d>fJSL3` ztPBt01|OcRA?D8)(pY^htW}G~*c`N!h*GbGr!z@HpBi#TEvjqKMM5%y*TS=Ek)Ip1 zRl2__Ji8XnuQ97Uwo3L^h0U0>Mug<2pT8c2T2b^F(MbnsRf|STcVu4TSsz}=R+l(d zZP{uiuIw(yTV7;!2@{1KqahtRo&~2Vb;lE)x;9~bcpH<04(Sii*w?iYovDS^hXYyX zQLQr$R(COZ+aW{PsubEzp2_OoTKVCp5M~_xWCZKOp-kGM6+$w?kxaU^jFB<5d>Fif zdzJK)>}7=GnC!q_UC3dO%$|>CBY%eDVO2y8!G4u~((xzd53|+6jzTg56-{HZp_?II z^gNTExMxK_89@-ez@++kL!xK}lhIZuU$m0RRvmLjkQ8lV@)_Qj5I=_0Rv1$F$p{KW z@75ZI2uo;^?iY%7WU(p~eas|gbyBpO$^O~w27Zc0pJuTt7JbPX+r3$I`uV3s2U#c9 z7pFu&GdVocwCmL9a28TBI?ALl<^}y^1f?TYn=&6GWup);b>IyyT&K`aKfipG$mAi1 z6lANfZC&M~BCN9<_i2q)aVA}L#>h`HTYY2mTs|tpWS%7zqH1h)fS;VuXsHyP0a8T$ zjAiTCUVv$U(cukK;VlqMu3og#XvY4p4!4W+4R zbiY!hZx`X6Ic7)J8;SnTR@h>UeUX-(W7wUz<1cIy-l-SqtRIbDrACImg=|SF{Y9`y zO(z@adRtoYfG{`GYrEp?%~4gs?un!Zsx`W4ETjtDa*`UTGv?!a5m>rGOH%qUSZZ47 zX-bJy6ZMiLrH|TDk*JiU2K@KUne!(Wl;;MD{7qtsEJT{ON+dV>?iG}p!gWk5ZKNJ} z7l~L_e>1E2UImtmzJt@Ndpj#z26mG)?Ew%RcGgF_Y+D;#htyf;V7Un%TnB5@kpjc@gOe+*1*0PhDS2JufpBr(%vvX_qri z={uoFZVpB+_S90Ytrd1#B2~pGA*q2XaIvpmh2&bijVJ!A*Oe}jQ)O46}%-@mY@ zzGB;<7;7Spx6*`K`2GtnEs8%2ASqNgcaXItR2y(-iB^?RCG}S7dq|$`Y0vgT12r6b zg&(=J4l;h2CDSu*hTg&qIEvELv=^~!66ug-=c1Lu@?oxubgGqntlq-b3>Dj0U&hNp zJQj;NUOV#2q+SnWTE>t_G)hR4Qt6{uztPGdjK-u$WFZo*3?k9WAQG(%BGJkq60Hm( z<<|Q$XQ8I_tg1N|$r?8OP5mNL#ZrDLpA{~^O#S=Pr&r&+1I)f;d9 z-Dm44Y4wt<-d$F&x79n0doB{yEw=P}TRO|KM=YBUiB=F<847cI+heySXVZPS`%#S9 zN28T0$x;=}GLZ`OH4;5frL(S^aaz&Kan|VXs70c09}$VZ%S9ymHWrcSFM36yZ*dWc zzGXn9wN|3<84yN)MJW<}(|}0yca$R0U;m0k-&P_LecOph^o=Sa(O>tH6mK479UGOn zMKl*gqOn1e=vB3)B2g(xQd+~7ibSO(Noj0-t__xe?_!Y^$(7b6Ds@_x(irQG>#j5N zQ2G`!boSrUI}5o#_c$%CEQGuG|FZ2ecxmwudimo|PhCL%oZU6;H!cE8O}iF<8za&^ zR{EihDZTX%BfW!NhUndO(6AeE&q~gt9U0|dahZ?E2Exp^8evUJ{*q({2Ns+F<(y*s+-V#>M+7_`=tJTJ0{T8EF=n*45 zfGZ5Lb#v7U(YNQe-I;2nL&%V@$FWlo=|SX5q#a|7^aJuD>^ z>9x6r?Z7+|b^}HyNv`cjSc^Ne#phP)J42YONBQf&2cYmGHIHG1=47K)fv`D}N1)Nm}{OQX4N+ygN)y+o0sUkkwxw_E!q;hlq6eb)}_F zb4AK((Vs2GWA`u8rg``lJZ5(tGOXh&!xjuT?2-ybT7+w2u^3y|usiA)i&Zs^w8%;$ zZ0U4s(OrAV8sA_WpGXv+NEDw)6rV^GpGXuHNv^-=F^2A8fBh?YTAGeABy1Nvi`3>$ zBRzxogwb_{NRJ_UlH&Dm1(mkjWm z^y(XXIL)<>#0cJ;hCdI z-(tjyR9Q<|ZRfFVLFA7lxAdw8xMt_l`dHUQs)D&M(sImSk*=_M&8+moL1X*#P9u%K zT_m@T>9}fpiR(DIxsg6P4_9r>cA*_2^{}Po%9+xn#zrcLcko279#(Xb8dx?MSrc{# zt{_EPP|-;1dz;eTSP!JMlaA_Gp>F6VowqjKliLNH0nFRft- z>al0F$#|v z3RtPA&C8~TjV=CARC~F=*72K-pyM#3*8y*N>UuFlMY`rWBYC=x*x4nkME7`N%pDQ5 zzI@HSMbAt`qGuc;(en?Hw%F2bwv?WrNNN5<#-iRFBcBF0KX7f^_Zz7?W;Zcc zZwT1WtTzv{R;2f_{)x17ut@RN&VP;NwvUMC*^T+HdH8-}j`@2;Pgf49Y4|xPytR1PmR7~;O?p*TVVpzhE@xGh0t?#vR8{$KoiY|u zyq4#_PfSho(@nq4ebh+vryJ?sNk;kv^H=(J?5T1EnVR+ho{|cisr3^0$%V|k?DyN_ z^}d@mdV*D$vqn#_L`rLDq)GLR)KE+D{KaDpRF8h1z5+V8n@FJ=c(SjGW$8C!$sEBv zoW)6U0Iwamm8_yHY44wnQJi$JLv{&u^IOiM7j)nIFb6Ik4SPY26w~VGPa;ypGfC^ zYb4tL32SDhD{LwCpp-5_R3cITinI~EFVYt5vqf4_*GLPoKM+Q36Ny?RQg7^dM7rfG zBP~Pp!YBtMx$U4_HRX1k{|Wv+jHT~zju+`3Scr7NkA`)&(!EwXZ1vh;CrB1<9rPvn zt=RLaMU6BgzmcBNQi4jtTGEzm)m)6V_AFgsS*@Ejb9P^}Zd}Qp>DjhOv|5NnYl%qo zG+ZS5>kyGnJ7`MpMs31quS}8~J@xc8T*u@ixGTgG^|?sY=OR&`i$r}c67{)A)aN3t zN3KNL@`v$9y)SGkuI@ydxz9*VG2b*hVk`>3W7u$9yGZHd>r5$)4Pi7kM4}NP()tD_ zzPGFejSVVwTTbV}&fMPJIID?7XGD?cj40Ar*os8wMv=N%_KUTpGoqBz8BruUBZ@?4 zM3Lx>C=#6!MWQpJNOVRNX(e(%k~}r<#?tuqxF5#Sp*|wHEuwpUeYi!>!iz}sy|E;@ zdNeEgvtH3dM!HVn4i&TRh*_is=p&JiqTfWK`6d$0Mv>0WZ>0Iy)eEDHlN72|UmIy7 zBpUajT87@Dr!ArC4K|qB9bk0U4OM3_x+Xc+N_a*~c;T1_VoZwk$H%xE#jNBqBYkIC zft|v#W@8vz6hxnkL|2d^(M%R;sg>wjl2}$Rqu2|LPLXJIibPoxseYnilr>?LHIXQ5 zB)OhxZjEElG%rbVOQ&J{P2kdJaE=$LUOQo~-iwQHmx=WbqNhco_ijY`2z7`=wTbks zl@1IxtS<5_rR#7`7O6f~0g*1YQhAJBVISGr@Ks2f6}P3c&|cA_D>RX4o{My-hGCRb zVU$ym-oW}I(i0aLM*A0G(`?%|S!u78w%dAXwGa!+y+~!yTO#e(EL00HI%(`SP;)W2 zra%hSSKr{C8zeW+K{?!C<2-L4Vx)Vu^v(CY#@5Bmx`-#2aPQ#2O?1fBF)A=QKb5Zj6{EPB=CIN#tTWIx(81wWX-umOIba?#-7Q7B)4=DM#)+( zoj(Kj*H~JiC0Fkw^hXAlK9?X;1GT8Dc|N_wN(C3-HPQ_u^?=ra9hC9!!SD{> zcq6@rJPZ4(Q@8b_pMSj|N0jcZtu zKF3Z=q~9^tM56aRM0y0PnMm{&2uW^K7ftt6P?0iT-AYBR)X7ToZ&g~azh$GWbdHu> z&lG(kdmf1SA<`%-Jz=Gd*r`hCP>g$#_U$qjU9cArw%xJ~7ah#*1w6`t2N0&BGGqk@3s*RRX)6TH$TFWlCQV%V; zF;k?c+6;+A84~HW!k(693^mf*mQ6v%MQ;(-Vv)+G8>!sgM!ErWRZ2&!Gtz`FO&xS! zR7&akQlu@mwu=3Y#U9Hl-yy7l>Tlcja1L!N!)<#9XBUyG#?iY9&q*q<-c44zz)BOW zbib8K;mj!(HLTRcN>lq8eKddJbCa9Kvvup_V*;0BL&cr0SUOu={ zsru}t{bEl`jf#lm>e1cRMyyA@DpKzhd^}`e3MJ7f16(dZfHC8%; zRaDq^>+b;@Uw<3HpB0S1{>Xt?BrY?y9W5({Yho$A=P^^d*~W6K)q87|DW(0mSiJOv zVF@;>f!N_mX;jEar&#F)YjKigKU>=bo87x?i_)x=Vzc%uW~kIr(dKXWWW!clY2+Nk ziee0rg*#qord-1P+XUlJB-+c96mM+AW3h1>uQac$g5AR%kH3d5%KEM|{%ylJES{dZ zfA87%s^Z1+PqVz{C2Mb8!M#PRib%6CD?~aI>xM|T&llDntB6t~lk4K_a2X^|b!0Mv z$(faibYUVo=Ya&Or$d^9gs>zoI&DF)STfn*4aCeL&xw}w18EPP80i6W4G3;;;K%h- zz*>Q2s zZFR`c-LS&t2YyUE_k*NZV*FImB<828)geCkpVc6^^v93ur-ddlKe#QXb;!^Auxf1y`S}6_OP1C#?YdHvn4eUuLw=6Hs-q?3 zr%+|wp|r&K>6OjT%~pr}RDo4*OUTbTAlSg+$8Fbrn#AHs(?rJM6|fp+3Hj*-@_;2K zp2?cT{5)oLD4zRa^@Jtl=Sh&KEiryx%;sm2)geEx!0IJS$j|#AuUlgLY|iFqi`5}N zKf?+aocJ;0uxJ&${b7mm^R*_ic70=Y$j_Ou`py!H=Q5BZmKZ+;JVNd~#m1D0h za6zL9`I!h(#uDSFPBuSvtq$i0R`o0)Kf6HAx5W6lESsOptq%E#Fx@&>LVjw1bhE_x zxx?$1LQzjr^sMj-=nVGyJERlH-QFOF+y*k-yVD_egN*WqIAj>e81FuZj0G9z4Rgq& zAmhFJ9Wo7Mf;ZYB3qdA%4``BbP$Hb_jdut|INh7z5Q=cNm+tsk2lAXZnTd>q%M_96 z4%x^$vmLS>WWKk6N$l+KytmZRp|isR?{$alhSfsv4TsSAVv+Z@L%xR2OWp@U66d^t zyDlIvdmk~`kT{?vzKNd6F40jfFt~zhq_anK)U?L9!@%S-P4<<*ct%|>dtYd)(Kvk+ zhR!l?zt(B|ym(&jeZ^!_Vg~kurJ=Lj`&N@HFISrUAVmEH&*fR?M@^1`R0es)``PHM zQmQ7%O7B-q_Jh;|dENV+$#8qlUG4c56n;i>?bOoMUc_Wl;%m5?I2%^0y+p0E7!it2 z0Zo3uGAyL9CbLT8xw=PIMKw9MF5*EQtG(iy)Li9haw?NaiPfOcnCmK~$@w5nnUv91 z9iGKgN{}^Pd95=Lqz#jbTBnZHsiMhSI0;9y3P9d=#hXQS6zlWR5ElPQ{P^Ezm{{e^W0V(c}cgTK_ z68;2DwsyeZU4oSI(;Y&7%J`EVLVn8nPiV3U*PBN{%KJ|;nWQS0!5PgXGDGXoiWFc2 zS>At2lb0~R5{xVHg>X@tfuV}JiwT!W9{z^?~%{&WM)&18s>Ci`XYWk}+`3&z{HiAwqf2}6F zu>#OItnIJY?(+No+Mf z&EGD55}!dlw631!f2awa3xwSiYAXi~3< zoQcl&8*5_v;(VX(d6}5|!s>j#xkCnlH1%5vQS>~D^3&YEK>IOuHTTgxA=c)()3dyeS8lw&~?F0 zzX-X_4>TFDPsjj2(&XCTh1}ui(_|tpP~L#&yZj_g_D)kwhWG_Ec|iNQ*Ds{WKy5YD zKZ!}K2Z#HoGKuAIxL@AUp&Z`tS91vEaHM}4lh|4}+CQ7gzNAZ^k#oUlzmXU@vmYsNe#ifCLe)3 z;M3hC>aUkpDRls3oPV7|4uL%EcX7yHAQSzr4#`&o-)-pkaLCCZPx!qYQVwLA-`gRl zgFNN;b;!9O&-k}%(xxl^Wgw6_{s4zu0W#Mg#AKgGf3rf_nD5`Ib!uXzy%sw2eY)dB z?J9i$qZ{N!|6WaKH68-8*iUoFD3F)^VGbcb%l#1!c?9G&eH63KFKbpx% zMPxR}dVj38qO0QtAaD2&I%GM>oBqQNSqJi#|ENQ@f^79CIpkxIcl^g4ascFgf2t;* z+$?o{;7@l5`T5YFVPWS_rClUE{N)dM-;FL6jSkZ=5D4!I2Ed;b+p>L!TQLH{*}kk!xrYKM^3 zVSk+_KmCEbuOPqsZ)h?c=RqQW_!~88gc05kLO1svk$d@2GS@fqe-9JaYq(@8VBVZ zLMzF6L5f4Zg;mp_qCU;G8qGN|g1s1|lI_&#Z>U33b9vyQc{mj(44QXQm2aE3$bfpiScVlqiRhA~g=x+ZAgkh7uFDQM`BW+2xE zjT~|bNSC0oL#_eo7Bq24FOXhAQ-@H-ZV8$@q(5}}1T7sxIlMh+?U4JR(;wfQExkiA zj|UkTv~>vO`HrBSL!N}rUBM*|c>(0!puIzugQNwQJ7g2c@SuZ3=x_T*1*uH-dc{+) z_JE8FuGZxD8|5nL!Jv~Si|5JC{K4RQO~zwnlGXU&22B#t3Ob)Y6m-?3^V7J(hR%eb zyCxs3!95T(`jOx!O&Z|I?Jv-IG`LxllDG=^6J%m=t0sxK3P`Ap^J&nJ$t0DCwU<`o z$AbP$WN%XlI!^?H98w%)N^qx;#Ov@^a%DlL1w)wZORA&y3Dbl7m`qCi2s`0Q(3u_# z(^h@4Pr$ONW(0KK9^*N&5}wUAhBPx6rAhF%>^GhY#xmKP)czFN|2!2us7dGV#LrW~ z!Q zF^OGU%@1a39a`sGK z&&J1sYzPpza-WPCB2Pvhh7P^~lXc_?=3^imEiq3bwq*13w$-608nnONW(hsfcou%% zvBda!KRCh>N=xbbYkLq>q53x@J%;;8w9nigBnYwB@7seUCX-Z`5!g|}^9MmehrAB* zQE-w&-U8Vjoa~StAbW#T9P$Oo7eTT^4uTvAN;~9FkZ*!=4#{5!`>3FTCg07#TWTOb z29+IB0pzEknnP-X{1Vi3$T=W~gE|hm0OUwe&motA91YHN$h9DU1`Qn23q*zIIE22H zBM2KiWGF}&p6`%xAPHe}hdd6F7`AfAY>)!sg${WUq+ocFLtX_b9A4s(H$jSo^qdZT zs>}*dEbJh}d+Y(ZYAY6ArL8isHh%!CV&OFo*$Z+?c%4JO0Z9(KIOG?QQejtzcy)2F zGVI}y0wCqXUJfY%QZel9kQ9*0VP7U2yf;=UzH3x1yq$@h3u-~9dU%(R#0Bl76*a

CRjIa3z!2{h#{b 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⪻e>4N?_Do;AwqxFIiN?6qWQ;%k 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 z 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 literal 0 HcmV?d00001 diff --git a/RobotNet.ServiceDefaults/Dockerfile b/RobotNet.ServiceDefaults/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/RobotNet.ServiceDefaults/Extensions.cs b/RobotNet.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..13151bf --- /dev/null +++ b/RobotNet.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj b/RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj new file mode 100644 index 0000000..2bcf451 --- /dev/null +++ b/RobotNet.ServiceDefaults/RobotNet.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/RobotNet.Shares/MessageResult.cs b/RobotNet.Shares/MessageResult.cs new file mode 100644 index 0000000..ef130b1 --- /dev/null +++ b/RobotNet.Shares/MessageResult.cs @@ -0,0 +1,8 @@ +namespace RobotNet.Shares; + +public record MessageResult(bool IsSuccess, string Message = ""); + +public record MessageResult(bool IsSuccess, string Message = "") +{ + public T? Data { get; set; } +} diff --git a/RobotNet.Shares/RobotNet.Shares.csproj b/RobotNet.Shares/RobotNet.Shares.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/RobotNet.Shares/RobotNet.Shares.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/RobotNet.Shares/SearchResult.cs b/RobotNet.Shares/SearchResult.cs new file mode 100644 index 0000000..0b28b9c --- /dev/null +++ b/RobotNet.Shares/SearchResult.cs @@ -0,0 +1,9 @@ +namespace RobotNet.Shares; + +public class SearchResult +{ + public int Total { get; set; } = 0; + public int Page { get; set; } = 1; + public int Size { get; set; } = 10; + public IEnumerable Items { get; set; } = []; +} diff --git a/RobotNet.SystemUpgrade/AppJsonSerializerContext.cs b/RobotNet.SystemUpgrade/AppJsonSerializerContext.cs new file mode 100644 index 0000000..27bcd59 --- /dev/null +++ b/RobotNet.SystemUpgrade/AppJsonSerializerContext.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.SystemUpgrade; + +public sealed record UploadFileResponse(string Name, long Size, string Url); +public sealed record FileItem(string Name, long Size, DateTime CreatedUtc, string Url); + +[JsonSerializable(typeof(UploadFileResponse))] +[JsonSerializable(typeof(FileItem))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/RobotNet.SystemUpgrade/Program.cs b/RobotNet.SystemUpgrade/Program.cs new file mode 100644 index 0000000..2ae9e68 --- /dev/null +++ b/RobotNet.SystemUpgrade/Program.cs @@ -0,0 +1,105 @@ +using Microsoft.AspNetCore.Http.Features; +using RobotNet.SystemUpgrade; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +builder.WebHost.ConfigureKestrel(o => +{ + o.Limits.MaxRequestBodySize = 2L * 1024 * 1024 * 1024; +}); + +builder.Services.Configure(o => +{ + o.MultipartBodyLengthLimit = 2L * 1024 * 1024 * 1024; +}); + +var app = builder.Build(); + + +var storeDir = Path.Combine(AppContext.BaseDirectory, "store"); +Directory.CreateDirectory(storeDir); + + +var apis = app.MapGroup("/api"); + + +var upload = apis.MapPost("/upload", async (HttpRequest request) => +{ + if (!request.HasFormContentType) + return Results.BadRequest("Content-Type phải là multipart/form-data."); + + var form = await request.ReadFormAsync(); + var file = form.Files.FirstOrDefault(); + if (file is null || file.Length == 0) + return Results.BadRequest("Không có file hoặc file rỗng."); + + var safeName = Path.GetFileName(file.FileName); + var savePath = Path.Combine(storeDir, safeName); + + await using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 64 * 1024, useAsync: true)) + await file.CopyToAsync(fs); + var nowUtc = DateTime.UtcNow; + try + { + System.IO.File.SetCreationTimeUtc(savePath, nowUtc); + System.IO.File.SetLastWriteTimeUtc(savePath, nowUtc); + } + catch + { + + } + var fi = new FileInfo(savePath); + var url = $"/api/files/{Uri.EscapeDataString(fi.Name)}"; + return Results.Created(url, new UploadFileResponse(fi.Name, fi.Length, url)); +}); + + +apis.MapGet("/files", () => +{ + var list = Directory.EnumerateFiles(storeDir) + .Select(p => new FileInfo(p)) + .OrderByDescending(f => f.CreationTimeUtc) + .Select(f => new FileItem( + f.Name, f.Length, f.CreationTimeUtc, + $"/api/files/{Uri.EscapeDataString(f.Name)}" + )) + .ToList(); + + return Results.Ok(list); +}); + + +apis.MapGet("/files/{name}", (string name) => +{ + var safeName = Path.GetFileName(name); + var path = Path.Combine(storeDir, safeName); + if (!System.IO.File.Exists(path)) + return Results.NotFound("Không tìm thấy file."); + + return Results.File(path, "application/octet-stream", fileDownloadName: safeName); +}); + + +apis.MapDelete("/files/{name}", (string name) => +{ + var safeName = Path.GetFileName(name); + var path = Path.Combine(storeDir, safeName); + if (!System.IO.File.Exists(path)) + return Results.NotFound("Không tìm thấy file."); + + System.IO.File.Delete(path); + return Results.NoContent(); +}); + + +app.UseStaticFiles(); +app.UseDefaultFiles(); +app.UseRouting(); +app.MapFallbackToFile("index.html"); + +app.Run(); diff --git a/RobotNet.SystemUpgrade/Properties/launchSettings.json b/RobotNet.SystemUpgrade/Properties/launchSettings.json new file mode 100644 index 0000000..9a3563a --- /dev/null +++ b/RobotNet.SystemUpgrade/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "index.html", + "workingDirectory": "$(TargetDir)", + "applicationUrl": "http://localhost:5296", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj b/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj new file mode 100644 index 0000000..a111d67 --- /dev/null +++ b/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.csproj @@ -0,0 +1,11 @@ + + + + net9.0 + enable + enable + true + true + + + diff --git a/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln b/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln new file mode 100644 index 0000000..d9a9a6d --- /dev/null +++ b/RobotNet.SystemUpgrade/RobotNet.SystemUpgrade.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36401.2 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.SystemUpgrade", "RobotNet.SystemUpgrade.csproj", "{D1EF28B0-A055-A148-A814-787BB9EC1F74}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1EF28B0-A055-A148-A814-787BB9EC1F74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1EF28B0-A055-A148-A814-787BB9EC1F74}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1EF28B0-A055-A148-A814-787BB9EC1F74}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1EF28B0-A055-A148-A814-787BB9EC1F74}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {65A7E842-E9B6-4B96-A47E-C20BC331568D} + EndGlobalSection +EndGlobal diff --git a/RobotNet.SystemUpgrade/appsettings.Development.json b/RobotNet.SystemUpgrade/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/RobotNet.SystemUpgrade/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RobotNet.SystemUpgrade/appsettings.json b/RobotNet.SystemUpgrade/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/RobotNet.SystemUpgrade/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/RobotNet.SystemUpgrade/libman.json b/RobotNet.SystemUpgrade/libman.json new file mode 100644 index 0000000..08e2c3c --- /dev/null +++ b/RobotNet.SystemUpgrade/libman.json @@ -0,0 +1,15 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.7", + "destination": "wwwroot/lib/bootstrap/" + }, + { + "provider": "jsdelivr", + "library": "@mdi/font@7.4.47", + "destination": "wwwroot/lib/mdi/font/" + } + ] +} \ No newline at end of file diff --git a/RobotNet.SystemUpgrade/readme.md b/RobotNet.SystemUpgrade/readme.md new file mode 100644 index 0000000..342951e --- /dev/null +++ b/RobotNet.SystemUpgrade/readme.md @@ -0,0 +1 @@ +dotnet publish -c Release -r linux-x64 --self-contained true -o ./bin/Release/net9.0/publish \ No newline at end of file diff --git a/RobotNet.SystemUpgrade/wwwroot/favicon.svg b/RobotNet.SystemUpgrade/wwwroot/favicon.svg new file mode 100644 index 0000000..b07dbc4 --- /dev/null +++ b/RobotNet.SystemUpgrade/wwwroot/favicon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/RobotNet.SystemUpgrade/wwwroot/index.html b/RobotNet.SystemUpgrade/wwwroot/index.html new file mode 100644 index 0000000..2b88c0f --- /dev/null +++ b/RobotNet.SystemUpgrade/wwwroot/index.html @@ -0,0 +1,553 @@ + + + + + File Upload Manager + + + + + + + +

+ +
+
+
+
+ + +
+ +
+ +
+ +
+
0%
+
+
+
+
+ + +
+
+
Uploaded Files
+
0 files
+
+
+
Loading files…
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/RobotNet.WebApp/App.razor b/RobotNet.WebApp/App.razor new file mode 100644 index 0000000..dcc26c7 --- /dev/null +++ b/RobotNet.WebApp/App.razor @@ -0,0 +1,25 @@ + + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
+ +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/RobotNet.WebApp/Charts/Components/BarChart.razor b/RobotNet.WebApp/Charts/Components/BarChart.razor new file mode 100644 index 0000000..2291d3b --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/BarChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/BarChart.razor.cs b/RobotNet.WebApp/Charts/Components/BarChart.razor.cs new file mode 100644 index 0000000..46632c8 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/BarChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BarChart; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class BarChart : RobotNetChart +{ + public BarChart() + { + chartType = ChartType.Bar; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (BarChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (BarChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is BarChartDataset barChartDataset && barChartDataset.Label == dataLabel) + if (data is BarChartDatasetData barChartDatasetData) + { + if (barChartDatasetData.Data is double dataValue) barChartDataset.Data?.Add(dataValue); + barChartDataset.BackgroundColor?.Add(barChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is BarChartDataset barChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is BarChartDatasetData barChartDatasetData && barChartDatasetData.DatasetLabel == barChartDataset.Label); + + if (chartDatasetData is BarChartDatasetData barChartDatasetData) + { + if (barChartDatasetData.Data is double dataValue) barChartDataset.Data?.Add(dataValue); + barChartDataset.BackgroundColor?.Add(barChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (BarChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is BarChartDataset barChartDataset) + { + chartData.Datasets.Add(barChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, barChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/BubbleChart.razor b/RobotNet.WebApp/Charts/Components/BubbleChart.razor new file mode 100644 index 0000000..dcfe8e1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/BubbleChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/BubbleChart.razor.cs b/RobotNet.WebApp/Charts/Components/BubbleChart.razor.cs new file mode 100644 index 0000000..6d5c3fd --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/BubbleChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BubbleChart; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class BubbleChart : RobotNetChart +{ + public BubbleChart() + { + chartType = ChartType.Bubble; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (BubbleChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (BubbleChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is BubbleChartDataset bubbleChartDataset && bubbleChartDataset.Label == dataLabel) + if (data is BubbleChartDatasetData bubbleChartDatasetData) + { + if (bubbleChartDatasetData.Data is BubblePoint dataValue) bubbleChartDataset.Data?.Add(dataValue); + bubbleChartDataset.BackgroundColor?.Add(bubbleChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is BubbleChartDataset bubbleChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is BubbleChartDatasetData bubbleChartDatasetData && bubbleChartDatasetData.DatasetLabel == bubbleChartDataset.Label); + + if (chartDatasetData is BubbleChartDatasetData bubbleChartDatasetData) + { + if (bubbleChartDatasetData.Data is BubblePoint dataValue) bubbleChartDataset.Data?.Add(dataValue); + bubbleChartDataset.BackgroundColor?.Add(bubbleChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (BubbleChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is BubbleChartDataset bubbleChartDataset) + { + chartData.Datasets.Add(bubbleChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, bubbleChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor b/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor new file mode 100644 index 0000000..dcfe8e1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor.cs b/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor.cs new file mode 100644 index 0000000..28c6567 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/ComboBarLineChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.ComboBarLineChart; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class ComboBarLineChart : RobotNetChart +{ + public ComboBarLineChart() + { + chartType = ChartType.Bar; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (ComboBarLineOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (ComboBarLineOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is ComboBarLineDataset comboBarLineChartDataset && comboBarLineChartDataset.Label == dataLabel) + if (data is ComboBarLineDatasetData comboBarLineChartDatasetData) + { + if (comboBarLineChartDatasetData.Data is double dataValue) comboBarLineChartDataset.Data?.Add(dataValue); + comboBarLineChartDataset.BackgroundColor?.Add(comboBarLineChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is ComboBarLineDataset comboBarLineChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is ComboBarLineDatasetData comboBarLineChartDatasetData && comboBarLineChartDatasetData.DatasetLabel == comboBarLineChartDataset.Label); + + if (chartDatasetData is ComboBarLineDatasetData comboBarLineChartDatasetData) + { + if (comboBarLineChartDatasetData.Data is double dataValue) comboBarLineChartDataset.Data?.Add(dataValue); + comboBarLineChartDataset.BackgroundColor?.Add(comboBarLineChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (ComboBarLineDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is ComboBarLineDataset comboBarLineChartDataset) + { + chartData.Datasets.Add(comboBarLineChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, comboBarLineChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/LineChart.razor b/RobotNet.WebApp/Charts/Components/LineChart.razor new file mode 100644 index 0000000..1237c4f --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/LineChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/LineChart.razor.cs b/RobotNet.WebApp/Charts/Components/LineChart.razor.cs new file mode 100644 index 0000000..18b840f --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/LineChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Models.LineChart; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class LineChart : RobotNetChart +{ + public LineChart() + { + chartType = ChartType.Line; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (LineChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (LineChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is LineChartDataset lineChartDataset && lineChartDataset.Label == dataLabel) + if (data is LineChartDatasetData lineChartDatasetData) + { + if (lineChartDatasetData.Data is double dataValue) lineChartDataset.Data?.Add(dataValue); + lineChartDataset.BackgroundColor?.Add(lineChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is LineChartDataset lineChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is LineChartDatasetData lineChartDatasetData && lineChartDatasetData.DatasetLabel == lineChartDataset.Label); + + if (chartDatasetData is LineChartDatasetData lineChartDatasetData) + { + if (lineChartDatasetData.Data is double dataValue) lineChartDataset.Data?.Add(dataValue); + lineChartDataset.BackgroundColor?.Add(lineChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (LineChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is LineChartDataset lineChartDataset) + { + chartData.Datasets.Add(lineChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, lineChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/PieChart.razor b/RobotNet.WebApp/Charts/Components/PieChart.razor new file mode 100644 index 0000000..9da6624 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/PieChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/PieChart.razor.cs b/RobotNet.WebApp/Charts/Components/PieChart.razor.cs new file mode 100644 index 0000000..729062f --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/PieChart.razor.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Models.PieChart; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class PieChart : RobotNetChart +{ + [Parameter] + public bool IsDoughnutChart { get; set; } = false; + public PieChart() + { + chartType = IsDoughnutChart ? ChartType.Doughnut : ChartType.Pie; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (PieChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (PieChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is PieChartDataset pieChartDataset && pieChartDataset.Label == dataLabel) + if (data is PieChartDatasetData pieChartDatasetData) + { + if (pieChartDatasetData.Data is double dataValue) pieChartDataset.Data?.Add(dataValue); + pieChartDataset.BackgroundColor?.Add(pieChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is PieChartDataset pieChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is PieChartDatasetData pieChartDatasetData && pieChartDatasetData.DatasetLabel == pieChartDataset.Label); + + if (chartDatasetData is PieChartDatasetData pieChartDatasetData) + { + if (pieChartDatasetData.Data is double dataValue) pieChartDataset.Data?.Add(dataValue); + pieChartDataset.BackgroundColor?.Add(pieChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (PieChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is PieChartDataset pieChartDataset) + { + chartData.Datasets.Add(pieChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, pieChartDataset); + } + + return chartData; + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor b/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor new file mode 100644 index 0000000..dcfe8e1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor.cs b/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor.cs new file mode 100644 index 0000000..e3a0251 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/PolarAreaChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Models.PolarAreaChart; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class PolarAreaChart : RobotNetChart +{ + public PolarAreaChart() + { + chartType = ChartType.PolarArea; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (PolarAreaChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (PolarAreaChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is PolarAreaChartDataset polarAreaChartDataset && polarAreaChartDataset.Label == dataLabel) + if (data is PolarAreaChartDatasetData polarAreaChartDatasetData) + { + if (polarAreaChartDatasetData.Data is double dataValue) polarAreaChartDataset.Data?.Add(dataValue); + polarAreaChartDataset.BackgroundColor?.Add(polarAreaChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is PolarAreaChartDataset polarAreaChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is PolarAreaChartDatasetData polarAreaChartDatasetData && polarAreaChartDatasetData.DatasetLabel == polarAreaChartDataset.Label); + + if (chartDatasetData is PolarAreaChartDatasetData polarAreaChartDatasetData) + { + if (polarAreaChartDatasetData.Data is double dataValue) polarAreaChartDataset.Data?.Add(dataValue); + polarAreaChartDataset.BackgroundColor?.Add(polarAreaChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (PolarAreaChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is PolarAreaChartDataset polarAreaChartDataset) + { + chartData.Datasets.Add(polarAreaChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, polarAreaChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/RadarChart.razor b/RobotNet.WebApp/Charts/Components/RadarChart.razor new file mode 100644 index 0000000..dcfe8e1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/RadarChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/RadarChart.razor.cs b/RobotNet.WebApp/Charts/Components/RadarChart.razor.cs new file mode 100644 index 0000000..8c8415a --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/RadarChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Models.RadarChart; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class RadarChart : RobotNetChart +{ + public RadarChart() + { + chartType = ChartType.Radar; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (RadarChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (RadarChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is RadarChartDataset radarChartDataset && radarChartDataset.Label == dataLabel) + if (data is RadarChartDatasetData radarChartDatasetData) + { + if (radarChartDatasetData.Data is double dataValue) radarChartDataset.Data?.Add(dataValue); + radarChartDataset.BackgroundColor?.Add(radarChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is RadarChartDataset radarChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is RadarChartDatasetData radarChartDatasetData && radarChartDatasetData.DatasetLabel == radarChartDataset.Label); + + if (chartDatasetData is RadarChartDatasetData radarChartDatasetData) + { + if (radarChartDatasetData.Data is double dataValue) radarChartDataset.Data?.Add(dataValue); + radarChartDataset.BackgroundColor?.Add(radarChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (RadarChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is RadarChartDataset radarChartDataset) + { + chartData.Datasets.Add(radarChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, radarChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Components/ScatterChart.razor b/RobotNet.WebApp/Charts/Components/ScatterChart.razor new file mode 100644 index 0000000..dcfe8e1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/ScatterChart.razor @@ -0,0 +1,7 @@ +@using RobotNet.WebApp.Charts.Core + +@inherits RobotNetChart + +
+ +
diff --git a/RobotNet.WebApp/Charts/Components/ScatterChart.razor.cs b/RobotNet.WebApp/Charts/Components/ScatterChart.razor.cs new file mode 100644 index 0000000..9ba871c --- /dev/null +++ b/RobotNet.WebApp/Charts/Components/ScatterChart.razor.cs @@ -0,0 +1,115 @@ +using Microsoft.JSInterop; +using RobotNet.WebApp.Charts.Core; +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Models.ScatterChart; + +namespace RobotNet.WebApp.Charts.Components; + +public partial class ScatterChart : RobotNetChart +{ + public ScatterChart() + { + chartType = ChartType.Scatter; + } + + public async Task InitializeAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.initialize", Id, chartType, data, (ScatterChartOptions)chartOptions); + } + } + + public async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) + { + if (chartData is not null && chartData.Datasets is not null) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync($"window.blazorChart.update", Id, data, (ScatterChartOptions)chartOptions); + } + } + + public async Task UpdateValuesAsync(ChartData chartData) + { + if (chartData is not null && chartData.Datasets is not null && chartData.Datasets.Count != 0) + { + var datasets = chartData.Datasets.OfType(); + var data = new { chartData.Labels, Datasets = datasets }; + await JSRuntime.InvokeVoidAsync("window.blazorChart.updateData", Id, data); + } + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IChartDatasetData data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(data); + + foreach (var dataset in chartData.Datasets) + if (dataset is ScatterChartDataset scatterChartDataset && scatterChartDataset.Label == dataLabel) + if (data is ScatterChartDatasetData scatterChartDatasetData) + { + if (scatterChartDatasetData.Data is ScatterChartDataPoint dataValue) scatterChartDataset.Data?.Add(dataValue); + scatterChartDataset.BackgroundColor?.Add(scatterChartDatasetData.BackgroundColor!); + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetData", Id, dataLabel, data); + return chartData; + } + + public async Task AddDataAsync(ChartData chartData, string dataLabel, IReadOnlyCollection data) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartData.Labels); + ArgumentNullException.ThrowIfNull(dataLabel); + ArgumentNullException.ThrowIfNull(data); + + if (string.IsNullOrWhiteSpace(dataLabel)) + throw new Exception($"{nameof(dataLabel)} cannot be empty."); + + if (data.Count == 0) + throw new Exception($"{nameof(data)} cannot be empty."); + + if (chartData.Datasets.Count != data.Count) + throw new InvalidDataException("The chart dataset count and the new data points count do not match."); + + if (chartData.Labels.Contains(dataLabel)) + throw new Exception($"{dataLabel} already exists."); + + chartData.Labels.Add(dataLabel); + + foreach (var dataset in chartData.Datasets) + if (dataset is ScatterChartDataset scatterChartDataset) + { + var chartDatasetData = data.FirstOrDefault(x => x is ScatterChartDatasetData scatterChartDatasetData && scatterChartDatasetData.DatasetLabel == scatterChartDataset.Label); + + if (chartDatasetData is ScatterChartDatasetData scatterChartDatasetData) + { + if (scatterChartDatasetData.Data is ScatterChartDataPoint dataValue) scatterChartDataset.Data?.Add(dataValue); + scatterChartDataset.BackgroundColor?.Add(scatterChartDatasetData.BackgroundColor!); + } + } + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDatasetsData", Id, dataLabel, data?.Select(x => (ScatterChartDatasetData)x)); + return chartData; + } + + public async Task AddDatasetAsync(ChartData chartData, IChartDataset chartDataset) + { + ArgumentNullException.ThrowIfNull(chartData); + ArgumentNullException.ThrowIfNull(chartData.Datasets); + ArgumentNullException.ThrowIfNull(chartDataset); + + if (chartDataset is ScatterChartDataset scatterChartDataset) + { + chartData.Datasets.Add(scatterChartDataset); + await JSRuntime.InvokeVoidAsync($"window.blazorChart.addDataset", Id, scatterChartDataset); + } + + return chartData; + } +} diff --git a/RobotNet.WebApp/Charts/Core/BlazorComponentBase.cs b/RobotNet.WebApp/Charts/Core/BlazorComponentBase.cs new file mode 100644 index 0000000..1cfc0de --- /dev/null +++ b/RobotNet.WebApp/Charts/Core/BlazorComponentBase.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace RobotNet.WebApp.Charts.Core; + +public abstract class BlazorComponentBase : ComponentBase, IDisposable, IAsyncDisposable +{ + private bool isAsyncDisposed; + + private bool isDisposed; + + /// + protected override async Task OnAfterRenderAsync(bool firstRender) + { + IsRenderComplete = true; + + await base.OnAfterRenderAsync(firstRender); + } + + /// + protected override void OnInitialized() + { + Id ??= Guid.NewGuid().ToString(); + + base.OnInitialized(); + } + + public static string BuildClassNames(params (string? cssClass, bool when)[] cssClassList) + { + var list = new HashSet(); + + if (cssClassList is not null && cssClassList.Length != 0) + foreach (var (cssClass, when) in cssClassList) + { + if (!string.IsNullOrWhiteSpace(cssClass) && when) + list.Add(cssClass); + } + + if (list.Count != 0) + return string.Join(" ", list); + else + return string.Empty; + } + + public static string BuildClassNames(string? userDefinedCssClass, params (string? cssClass, bool when)[] cssClassList) + { + var list = new HashSet(); + + if (cssClassList is not null && cssClassList.Length != 0) + foreach (var (cssClass, when) in cssClassList) + { + if (!string.IsNullOrWhiteSpace(cssClass) && when) + list.Add(cssClass); + } + + if (!string.IsNullOrWhiteSpace(userDefinedCssClass)) + list.Add(userDefinedCssClass.Trim()); + + if (list.Count != 0) + return string.Join(" ", list); + else + return string.Empty; + } + + public static string BuildStyleNames(string? userDefinedCssStyle, params (string? cssStyle, bool when)[] cssStyleList) + { + var list = new HashSet(); + + if (cssStyleList is not null && cssStyleList.Length != 0) + foreach (var (cssStyle, when) in cssStyleList) + { + if (!string.IsNullOrWhiteSpace(cssStyle) && when) + list.Add(cssStyle); + } + + if (!string.IsNullOrWhiteSpace(userDefinedCssStyle)) + list.Add(userDefinedCssStyle.Trim()); + + if (list.Count != 0) + return string.Join(';', list); + else + return string.Empty; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(true).ConfigureAwait(false); + + Dispose(false); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed) + { + if (disposing) + { + // cleanup + } + + isDisposed = true; + } + } + + protected virtual ValueTask DisposeAsyncCore(bool disposing) + { + if (!isAsyncDisposed) + { + if (disposing) + { + // cleanup + } + + isAsyncDisposed = true; + } + + return ValueTask.CompletedTask; + } + + [Parameter(CaptureUnmatchedValues = true)] public Dictionary AdditionalAttributes { get; set; } = default!; + + [Parameter] public string? Class { get; set; } + + protected virtual string? ClassNames => Class; + + public ElementReference Element { get; set; } + + [Parameter] public string? Id { get; set; } + + protected bool IsRenderComplete { get; private set; } + + [Inject] protected IJSRuntime JSRuntime { get; set; } = default!; + + [Parameter] public string? Style { get; set; } + + protected virtual string? StyleNames => Style; + + ~BlazorComponentBase() + { + Dispose(false); + } +} diff --git a/RobotNet.WebApp/Charts/Core/ChartColors.cs b/RobotNet.WebApp/Charts/Core/ChartColors.cs new file mode 100644 index 0000000..e170d0e --- /dev/null +++ b/RobotNet.WebApp/Charts/Core/ChartColors.cs @@ -0,0 +1,22 @@ +namespace RobotNet.WebApp.Charts.Core; + +public static class ChartColors +{ + public static readonly System.Drawing.Color RedRGB = System.Drawing.Color.FromArgb(255, 0, 0); + public static readonly System.Drawing.Color YellowRGB = System.Drawing.Color.FromArgb(230, 162, 60); + public static readonly System.Drawing.Color GreenRGB = System.Drawing.Color.FromArgb(0, 204, 0); + public static readonly System.Drawing.Color OrangeRGB = System.Drawing.Color.FromArgb(255, 102, 0); + public static readonly System.Drawing.Color GrayRGB = System.Drawing.Color.FromArgb(147, 147, 147); + + public static readonly string RedStr = "#ff0000"; + public static readonly string YellowStr = "#e6a23c"; + public static readonly string GreenStr = "#00cc00"; + public static readonly string OrangeStr = "#ff6600"; + public static readonly string GrayStr = "#939393"; + public static readonly string BlueSkyStr = "#29a3ff"; + public static readonly string BlueStr = "#223771"; + public static readonly string Robot01Str = "#6300bd"; + public static readonly string Robot02Str = "#8a2e47"; + + public static readonly List ColorList = [Robot01Str, Robot02Str, BlueSkyStr, GrayStr, OrangeStr]; +} diff --git a/RobotNet.WebApp/Charts/Core/RobotNetChart.cs b/RobotNet.WebApp/Charts/Core/RobotNetChart.cs new file mode 100644 index 0000000..54f396e --- /dev/null +++ b/RobotNet.WebApp/Charts/Core/RobotNetChart.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Components; +using RobotNet.WebApp.Charts.Enums; +using System.Globalization; + +namespace RobotNet.WebApp.Charts.Core; + +public class RobotNetChart : BlazorComponentBase, IDisposable, IAsyncDisposable +{ + internal ChartType chartType; + + /// + public new virtual void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public new virtual async ValueTask DisposeAsync() + { + await DisposeAsyncCore(true); + Dispose(false); + GC.SuppressFinalize(this); + } + + private string GetChartContainerSizeAsStyle() + { + var style = ""; + + if (Width > 0) + style += $"width:{Width.Value.ToString(CultureInfo.InvariantCulture)}{WidthUnit.ToCssString()};"; + + if (Height > 0) + style += $"height:{Height.Value.ToString(CultureInfo.InvariantCulture)}{HeightUnit.ToCssString()};"; + + return style; + } + + internal string ContainerStyle => GetChartContainerSizeAsStyle(); + + /// + /// Gets or sets chart container height. + /// The default unit of measure is . + /// To change the unit of measure see . + /// + /// + /// Default value is null. + /// + [Parameter] + public int? Height { get; set; } + + /// + /// Gets or sets chart container height unit of measure. + /// + /// + /// Default value is . + /// + [Parameter] + public Unit HeightUnit { get; set; } = Unit.Px; + + /// + /// Get or sets chart container width. + /// The default unit of measure is . + /// To change the unit of measure see . + /// + /// + /// Default value is null. + /// + [Parameter] + public int? Width { get; set; } + + /// + /// Gets or sets chart container width unit of measure. + /// + /// + /// Default value is . + /// + [Parameter] + public Unit WidthUnit { get; set; } = Unit.Px; +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Enums/AxisDirection.cs b/RobotNet.WebApp/Charts/Enums/AxisDirection.cs new file mode 100644 index 0000000..0b8283a --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/AxisDirection.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum AxisDirection +{ + X, + Y, + XY +} diff --git a/RobotNet.WebApp/Charts/Enums/BorderAlign.cs b/RobotNet.WebApp/Charts/Enums/BorderAlign.cs new file mode 100644 index 0000000..3093ed0 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/BorderAlign.cs @@ -0,0 +1,7 @@ +namespace RobotNet.WebApp.Charts.Enums; + +public enum BorderAlign +{ + Center, + Inner +} diff --git a/RobotNet.WebApp/Charts/Enums/BorderJoinStyle.cs b/RobotNet.WebApp/Charts/Enums/BorderJoinStyle.cs new file mode 100644 index 0000000..9c3d389 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/BorderJoinStyle.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum BorderJoinStyle +{ + Round, + Bevel, + Miter +} diff --git a/RobotNet.WebApp/Charts/Enums/BorderSkipped.cs b/RobotNet.WebApp/Charts/Enums/BorderSkipped.cs new file mode 100644 index 0000000..e74ef30 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/BorderSkipped.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum BorderSkipped +{ + Start, + End, + Middle, + Bottom, + Left, + Top, + Right, + False, + True +} diff --git a/RobotNet.WebApp/Charts/Enums/ChartAxesType.cs b/RobotNet.WebApp/Charts/Enums/ChartAxesType.cs new file mode 100644 index 0000000..637d3fb --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/ChartAxesType.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum ChartAxesType +{ + Linear, + Logarithmic, + Category, + Time, + Timeseries, +} diff --git a/RobotNet.WebApp/Charts/Enums/ChartType.cs b/RobotNet.WebApp/Charts/Enums/ChartType.cs new file mode 100644 index 0000000..9a28141 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/ChartType.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(CamelCaseEnumConverter))] +public enum ChartType +{ + Line, + Bar, + Pie, + Doughnut, + PolarArea, + Radar, + Scatter, + Bubble +} + diff --git a/RobotNet.WebApp/Charts/Enums/DataLabelsAlignment.cs b/RobotNet.WebApp/Charts/Enums/DataLabelsAlignment.cs new file mode 100644 index 0000000..9448630 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/DataLabelsAlignment.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum DataLabelsAlignment +{ + Center, + Start, + End, + Right, + Bottom, + Left, + Top, + None +} diff --git a/RobotNet.WebApp/Charts/Enums/DataLabelsAnchoring.cs b/RobotNet.WebApp/Charts/Enums/DataLabelsAnchoring.cs new file mode 100644 index 0000000..340d305 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/DataLabelsAnchoring.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum DataLabelsAnchoring +{ + Center, + Start, + End, + None, +} diff --git a/RobotNet.WebApp/Charts/Enums/DatasetType.cs b/RobotNet.WebApp/Charts/Enums/DatasetType.cs new file mode 100644 index 0000000..88f9d67 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/DatasetType.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum DatasetType +{ + Bar, + Line, +} diff --git a/RobotNet.WebApp/Charts/Enums/Easing.cs b/RobotNet.WebApp/Charts/Enums/Easing.cs new file mode 100644 index 0000000..4c077ae --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/Easing.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum Easing +{ + Linear, + EaseInQuad, + EaseOutQuad, + EaseInOutQuad, + EaseInCubic, + EaseOutCubic, + EaseInOutCubic, + EaseInQuart, + EaseOutQuart, + EaseInOutQuart, + EaseInQuint, + EaseOutQuint, + EaseInOutQuint, + EaseInSine, + EaseOutSine, + EaseInOutSine, + EaseInExpo, + EaseOutExpo, + EaseInOutExpo, + EaseInCirc, + EaseOutCirc, + EaseInOutCirc, + EaseInElastic, + EaseOutElastic, + EaseInOutElastic, + EaseInBack, + EaseOutBack, + EaseInOutBack, + EaseInBounce, + EaseOutBounce, + EaseInOutBounce, +} diff --git a/RobotNet.WebApp/Charts/Enums/EnumExtensions.cs b/RobotNet.WebApp/Charts/Enums/EnumExtensions.cs new file mode 100644 index 0000000..c190e00 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/EnumExtensions.cs @@ -0,0 +1,38 @@ +namespace RobotNet.WebApp.Charts.Enums; + +public static class EnumExtensions +{ + + public static string? ToChartDatasetDataLabelAlignmentString(this DataLabelsAlignment alignment) => + alignment switch + { + DataLabelsAlignment.Start => "start", + DataLabelsAlignment.Center or DataLabelsAlignment.None => "center", // default + DataLabelsAlignment.End => "end", + _ => null + }; + + public static string? ToChartDatasetDataLabelAnchorString(this DataLabelsAnchoring anchor) => + anchor switch + { + DataLabelsAnchoring.Start => "start", + DataLabelsAnchoring.Center or DataLabelsAnchoring.None => "center", // default + DataLabelsAnchoring.End => "end", + _ => null + }; + + public static string ToCssString(this Unit unit) => + unit switch + { + Unit.Em => "em", + Unit.Percentage => "%", + Unit.Pt => "pt", + Unit.Px => "px", + Unit.Rem => "rem", + Unit.Vh => "vh", + Unit.VMax => "vmax", + Unit.VMin => "vmin", + Unit.Vw => "vw", + _ => string.Empty + }; +} diff --git a/RobotNet.WebApp/Charts/Enums/IndexAxis.cs b/RobotNet.WebApp/Charts/Enums/IndexAxis.cs new file mode 100644 index 0000000..d7c21e5 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/IndexAxis.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum IndexAxis +{ + X, + Y, +} diff --git a/RobotNet.WebApp/Charts/Enums/InteractionMode.cs b/RobotNet.WebApp/Charts/Enums/InteractionMode.cs new file mode 100644 index 0000000..5ef9221 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/InteractionMode.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum InteractionMode +{ + /// + /// Finds items in the same dataset. + /// + Dataset, + + /// + /// Finds item at the same index. + /// + Index, + + /// + /// Gets the items that are at the nearest distance to the point. + /// + Nearest, + + /// + /// Finds all of the items that intersect the point + /// + Point, + + /// + /// Returns all items that would intersect based on the X coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts. + /// + X, + + /// + /// Returns all items that would intersect based on the Y coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. + /// + Y +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Enums/LegendAlignment.cs b/RobotNet.WebApp/Charts/Enums/LegendAlignment.cs new file mode 100644 index 0000000..2025099 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/LegendAlignment.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum LegendAlignment +{ + Start, + Center, + End +} diff --git a/RobotNet.WebApp/Charts/Enums/LegendPosition.cs b/RobotNet.WebApp/Charts/Enums/LegendPosition.cs new file mode 100644 index 0000000..84ffccd --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/LegendPosition.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum LegendPosition +{ + Top, + Left, + Bottom, + Right, + /// + /// Using the 'chartArea' option the legend position is at the moment not configurable, it will always be on the left side of the chart in the middle. + /// + ChartArea, +} diff --git a/RobotNet.WebApp/Charts/Enums/LowerCaseEnumConverter.cs b/RobotNet.WebApp/Charts/Enums/LowerCaseEnumConverter.cs new file mode 100644 index 0000000..e518442 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/LowerCaseEnumConverter.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +public class LowerCaseEnumConverter : JsonConverter where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetString(); + if (Enum.TryParse(value, true, out T result)) + { + return result; + } + throw new JsonException($"Unable to convert \"{value}\" to enum {typeof(T)}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString().ToLower()); + } +} + +public class CamelCaseEnumConverter : JsonConverter where T : struct, Enum +{ + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + string value = reader.GetString() ?? throw new JsonException(); + foreach (T enumValue in Enum.GetValues()) + { + if (string.Equals(CamelCaseEnumConverter.ToCamelCase(enumValue.ToString()), value, StringComparison.OrdinalIgnoreCase)) + { + return enumValue; + } + } + throw new JsonException($"Unable to convert \"{value}\" to {typeof(T)}."); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + string enumString = CamelCaseEnumConverter.ToCamelCase(value.ToString()); + writer.WriteStringValue(enumString); + } + + private static string ToCamelCase(string input) + { + if (string.IsNullOrEmpty(input) || char.IsLower(input[0])) + return input; + + return char.ToLower(input[0]) + input[1..]; + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Enums/PointStyle.cs b/RobotNet.WebApp/Charts/Enums/PointStyle.cs new file mode 100644 index 0000000..4e9506d --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/PointStyle.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum PointStyle +{ + Circle, + Cross, + CrossRot, + Dash, + Line, + Rect, + RectRounded, + RectRot, + Star, + Triangle, +} diff --git a/RobotNet.WebApp/Charts/Enums/TicksAlignment.cs b/RobotNet.WebApp/Charts/Enums/TicksAlignment.cs new file mode 100644 index 0000000..3468e65 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/TicksAlignment.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TicksAlignment +{ + Center, + Start, + End +} diff --git a/RobotNet.WebApp/Charts/Enums/TitleAlignment.cs b/RobotNet.WebApp/Charts/Enums/TitleAlignment.cs new file mode 100644 index 0000000..ee3dcd3 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/TitleAlignment.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TitleAlignment +{ + Start, + Center, + End +} diff --git a/RobotNet.WebApp/Charts/Enums/TitlePosition.cs b/RobotNet.WebApp/Charts/Enums/TitlePosition.cs new file mode 100644 index 0000000..2b211d4 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/TitlePosition.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TitlePosition +{ + Top, + Left, + Bottom, + Right, +} diff --git a/RobotNet.WebApp/Charts/Enums/TooltipAlignment.cs b/RobotNet.WebApp/Charts/Enums/TooltipAlignment.cs new file mode 100644 index 0000000..4577907 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/TooltipAlignment.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TooltipXAlignment +{ + Left, + Center, + Right, +} + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TooltipYAlignment +{ + Top, + Center, + Bottom, +} diff --git a/RobotNet.WebApp/Charts/Enums/TooltipPosition.cs b/RobotNet.WebApp/Charts/Enums/TooltipPosition.cs new file mode 100644 index 0000000..37643f7 --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/TooltipPosition.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum TooltipPosition +{ + Average, + Nearest +} diff --git a/RobotNet.WebApp/Charts/Enums/Unit.cs b/RobotNet.WebApp/Charts/Enums/Unit.cs new file mode 100644 index 0000000..8d6b28c --- /dev/null +++ b/RobotNet.WebApp/Charts/Enums/Unit.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Enums; + +[JsonConverter(typeof(LowerCaseEnumConverter))] +public enum Unit +{ + /// + /// No unit. + /// + None, + + /// + /// Ems. + /// + Em, + + /// + /// Percent. + /// + Percentage, + + /// + /// Points. + /// + Pt, + + /// + /// Pixels. + /// + Px, + + /// + /// Root ems. + /// + Rem, + + /// + /// Viewport height. + /// + Vh, + + /// + /// Viewport maximum height. + /// + VMax, + + /// + /// Viewport minimum height. + /// + VMin, + + /// + /// Viewport width. + /// + Vw +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxes.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxes.cs new file mode 100644 index 0000000..e843763 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxes.cs @@ -0,0 +1,86 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxes +{ + /// + /// If true, scale will include 0 if it is not already included. + /// + public bool BeginAtZero { get; set; } = true; + + /// + /// Define options for the border that run perpendicular to the axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesBorder? Border { get; set; } + + /// + /// Gets or sets the grid configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesGrid? Grid { get; set; } + + /// + /// User defined maximum number for the scale, overrides maximum value from data. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Max { get; set; } + + /// + /// User defined minimum number for the scale, overrides minimum value from data. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Min { get; set; } + + /// + /// Should the data be stacked. + /// By default data is not stacked. + /// If the stacked option of the value scale (y-axis on horizontal chart) is true, positive and negative values are stacked + /// separately. + /// + public bool Stacked { get; set; } + + /// + /// Adjustment used when calculating the maximum data value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? SuggestedMax { get; set; } + + /// + /// Adjustment used when calculating the minimum data value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? SuggestedMin { get; set; } + + /// + /// Gets or sets the tick configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesTicks? Ticks { get; set; } + + /// + /// Gets or sets the title configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesTitle? Title { get; set; } + + /// + /// Gets or sets the index scale type. />. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesType? Type { get; set; } = null; + + /// + /// The weight used to sort the axis. Higher weights are further away from the chart area. + /// + public int? Weight { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TitlePosition? Position { get; set; } +} + diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesBorder.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesBorder.cs new file mode 100644 index 0000000..5d91c98 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesBorder.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxesBorder +{ + /// + /// The color of the border line. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Length and spacing of dashes on grid lines + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Dash { get; set; } + + /// + /// Offset for line dashes. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? DashOffset { get; set; } + + /// + /// If , draw a border at the edge between the axis and the chart area. + /// + public bool Display { get; set; } = true; + + /// + /// The width of the border line. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Width { get; set; } + + /// + /// z-index of the border layer. Values <= 0 are drawn under datasets, > 0 on top. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Z { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesGrid.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesGrid.cs new file mode 100644 index 0000000..a979c47 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesGrid.cs @@ -0,0 +1,82 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxesGrid +{ + /// + /// If , gridlines are circular (on radar and polar area charts only). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Circular { get; set; } + + /// + /// Color of the grid axis lines. Here one can write a CSS method or even a JavaScript method for a dynamic color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// If false, do not display grid lines for this axis. + /// + public bool Display { get; set; } = true; + + /// + /// If , draw lines on the chart area inside the axis lines. This is useful when there are multiple + /// axes and you need to control which grid lines are drawn. + /// + public bool DrawOnChartArea { get; set; } = true; + + /// + /// If , draw lines beside the ticks in the axis area beside the chart. + /// + public bool DrawTicks { get; set; } = true; + + /// + /// Stroke width of grid lines. + /// + public int LineWidth { get; set; } = 1; + + /// + /// If , grid lines will be shifted to be between labels. This is set to true for a bar chart by + /// default. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Offset { get; set; } = false; + + /// + /// Length and spacing of the tick mark line. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? TickBorderDash { get; set; } + + /// + /// Offset for the line dash of the tick mark. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TickBorderDashOffset { get; set; } + + /// + /// Color of the tick line. If unset, defaults to the grid line color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TickColor { get; set; } + + /// + /// Length in pixels that the grid lines will draw into the axis area. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TickLength { get; set; } + + /// + /// Width of the tick mark in pixels. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TickWidth { get; set; } + + /// + /// z-index of the gridline layer. Values <= 0 are drawn under datasets, > 0 on top. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Z { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicks.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicks.cs new file mode 100644 index 0000000..f034460 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicks.cs @@ -0,0 +1,64 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxesTicks +{ + [JsonPropertyName("align")] + public TicksAlignment Alignment { get; set; } = TicksAlignment.Center; + + /// + /// Color of label backdrops + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackdropColor { get; set; } + + /// + /// Padding of label backdrop + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BackdropPadding { get; set; } + + /// + /// Color of ticks + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// If , show tick labels. + /// + public bool Display { get; set; } = true; + + /// + /// defines options for the major tick marks that are generated by the axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxesTicksMajor? Major { get; set; } + + /// + /// Sets the offset of the tick labels from the axis + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + + /// + /// If , draw a background behind the tick labels. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? ShowLabelBackdrop { get; set; } + + /// + /// The color of the stroke around the text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TextStrokeColor { get; set; } + + /// + /// Stroke width around the text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TextStrokeWidth { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicksMajor.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicksMajor.cs new file mode 100644 index 0000000..e98c179 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTicksMajor.cs @@ -0,0 +1,10 @@ +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxesTicksMajor +{ + /// + /// If , major ticks are generated. A major tick will affect auto skipping and major will be defined + /// on ticks in the scriptable options context. + /// + public bool Enabled { get; set; } = false; +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTitle.cs b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTitle.cs new file mode 100644 index 0000000..6dff924 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/Axes/ChartAxesTitle.cs @@ -0,0 +1,44 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart.Axes; + +public class ChartAxesTitle +{ + /// + /// Alignment of the title. + /// Options are: 'start', 'center', and 'end' + /// + [JsonPropertyName("align")] + public TitleAlignment Alignment { get; set; } = TitleAlignment.Center; + + /// + /// Color of text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } = "black"; + + /// + /// Is the title shown? + /// + public bool Display { get; set; } = true; + + public ChartFont? Font { get; set; } = new(); + + public bool FullSize { get; set; } = true; + + /// + /// Gets or sets the position of the title. + /// + public TitlePosition Position { get; set; } = TitlePosition.Top; + + /// + /// Gets or sets the number of pixels to add above and below the title text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/BarChartDataset.cs b/RobotNet.WebApp/Charts/Models/BarChart/BarChartDataset.cs new file mode 100644 index 0000000..d10d2e9 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/BarChartDataset.cs @@ -0,0 +1,118 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart; + +public class BarChartDataset : ChartDataset +{ + /// + /// Percent (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + /// + /// Default value is 0.9. + /// + public double BarPercentage { get; set; } = 0.9; + + /// + /// It is applied to the width of each bar, in pixels. + /// When this is enforced, barPercentage and categoryPercentage are ignored. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? BarThickness { get; set; } + + /// + /// This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BorderSkipped? BorderSkipped { get; set; } + + /// + /// Percent (0-1) of the available width each category should be within the sample width. + /// + /// + /// Default value is 0.8. + /// + public double CategoryPercentage { get; set; } = 0.8; + + /// + /// Should the bars be grouped on index axis. + /// When , all the datasets at same index value will be placed next to each other centering on that + /// index value. + /// When , each bar is placed on its actual index-axis value. + /// + /// + /// Default value is . + /// + public bool Grouped { get; set; } = true; + + /// + /// The bar border radius when hovered (in pixels). + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderRadius { get; set; } + + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IndexAxis? IndexAxis { get; set; } + + /// + /// Set this to ensure that bars are not sized thicker than this. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? MaxBarThickness { get; set; } + + /// + /// Set this to ensure that bars have a minimum length in pixels. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? MinBarLength { get; set; } + + public PointStyle PointStyle { get; set; } = PointStyle.Circle; + + /// + /// If , null or undefined values will not be used for spacing calculations when determining bar + /// size. + /// + /// + /// Default value is . + /// + public bool SkipNull { get; set; } + + /// + /// The ID of the x axis to plot this dataset on. + /// + /// + /// Default value is first x axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? XAxisID { get; set; } + + /// + /// The ID of the y axis to plot this dataset on. + /// + /// + /// Default value is first y axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? YAxisID { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/BarChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/BarChart/BarChartDatasetData.cs new file mode 100644 index 0000000..4c0f89e --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/BarChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.BarChart; + +public class BarChartDatasetData(string? datasetLabel, object data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/BarChartOptions.cs b/RobotNet.WebApp/Charts/Models/BarChart/BarChartOptions.cs new file mode 100644 index 0000000..b78c43f --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/BarChartOptions.cs @@ -0,0 +1,21 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.BarChart; + +public class BarChartOptions : ChartOptions +{ + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + public IndexAxis IndexAxis { get; set; } = IndexAxis.X; + + public BarChartPlugins Plugins { get; set; } = new(); + + public BarScales Scales { get; set; } = new(); + +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/BarChartPlugins.cs b/RobotNet.WebApp/Charts/Models/BarChart/BarChartPlugins.cs new file mode 100644 index 0000000..e3b9dc1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/BarChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.BarChart; + +public class BarChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/BarChart/BarScales.cs b/RobotNet.WebApp/Charts/Models/BarChart/BarScales.cs new file mode 100644 index 0000000..c8d1303 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BarChart/BarScales.cs @@ -0,0 +1,13 @@ +using RobotNet.WebApp.Charts.Models.BarChart.Axes; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BarChart; + +public class BarScales +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxes? X { get; set; } = new(); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxes? Y { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDataset.cs b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDataset.cs new file mode 100644 index 0000000..d73269b --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDataset.cs @@ -0,0 +1,18 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BubbleChart; + +public class BubbleChartDataset : ChartDataset +{ + public bool DrawActiveElementsOnTop { get; set; } = true; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? HoverRadius { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? HitRadius { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Radius { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDatasetData.cs new file mode 100644 index 0000000..0164381 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.BubbleChart; + +public class BubbleChartDatasetData(string? datasetLabel, BubblePoint data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartOptions.cs b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartOptions.cs new file mode 100644 index 0000000..624b15f --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartOptions.cs @@ -0,0 +1,21 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BarChart; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.BubbleChart; + +public class BubbleChartOptions : ChartOptions +{ + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + public IndexAxis IndexAxis { get; set; } = IndexAxis.X; + + public BubbleChartPlugins Plugins { get; set; } = new(); + + public BarScales Scales { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartPlugins.cs b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartPlugins.cs new file mode 100644 index 0000000..4237a87 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BubbleChart/BubbleChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.BubbleChart; + +public class BubbleChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/BubbleChart/BubblePoint.cs b/RobotNet.WebApp/Charts/Models/BubbleChart/BubblePoint.cs new file mode 100644 index 0000000..bd79cbd --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/BubbleChart/BubblePoint.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.BubbleChart; + +public class BubblePoint(double x, double y, double r) +{ + /// + /// Gets the X-value of this . + /// + public double X { get; } = x; + + /// + /// Gets the Y-value of this . + /// + public double Y { get; } = y; + + /// + /// Gets the radius of this in pixels. Will be serialized as 'r'. + /// + /// Important: this property is not scaled by the chart, + /// it is the raw radius in pixels of the bubble that is drawn on the canvas. + /// + /// + [JsonPropertyName("r")] + public double Radius { get; } = r; +} diff --git a/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDataset.cs b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDataset.cs new file mode 100644 index 0000000..cfddf34 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDataset.cs @@ -0,0 +1,116 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.ComboBarLineChart; + +public class ComboBarLineDataset : ChartDataset +{ + /// + /// Percent (0-1) of the available width each bar should be within the category width. + /// 1.0 will take the whole category width and put the bars right next to each other. + /// + /// + /// Default value is 0.9. + /// + public double BarPercentage { get; set; } = 0.9; + + /// + /// It is applied to the width of each bar, in pixels. + /// When this is enforced, barPercentage and categoryPercentage are ignored. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? BarThickness { get; set; } + + /// + /// This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius. + /// + public BorderSkipped BorderSkipped { get; set; } = BorderSkipped.Start; + + /// + /// Percent (0-1) of the available width each category should be within the sample width. + /// + /// + /// Default value is 0.8. + /// + public double CategoryPercentage { get; set; } = 0.8; + + /// + /// Should the bars be grouped on index axis. + /// When , all the datasets at same index value will be placed next to each other centering on that + /// index value. + /// When , each bar is placed on its actual index-axis value. + /// + /// + /// Default value is . + /// + public bool Grouped { get; set; } = true; + + /// + /// The bar border radius when hovered (in pixels). + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderRadius { get; set; } + + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + public IndexAxis IndexAxis { get; set; } = IndexAxis.X; + + /// + /// Set this to ensure that bars are not sized thicker than this. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? MaxBarThickness { get; set; } + + /// + /// Set this to ensure that bars have a minimum length in pixels. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? MinBarLength { get; set; } + + public PointStyle PointStyle { get; set; } = PointStyle.Circle; + + /// + /// If , null or undefined values will not be used for spacing calculations when determining bar + /// size. + /// + /// + /// Default value is . + /// + public bool SkipNull { get; set; } + + /// + /// The ID of the x axis to plot this dataset on. + /// + /// + /// Default value is first x axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? XAxisID { get; set; } + + /// + /// The ID of the y axis to plot this dataset on. + /// + /// + /// Default value is first y axis. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? YAxisID { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDatasetData.cs b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDatasetData.cs new file mode 100644 index 0000000..8a231e3 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.ComboBarLineChart; + +public class ComboBarLineDatasetData(string? datasetLabel, object data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineOptions.cs b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineOptions.cs new file mode 100644 index 0000000..69b0f41 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineOptions.cs @@ -0,0 +1,21 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BarChart; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.ComboBarLineChart; + +public class ComboBarLineOptions : ChartOptions +{ + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + public IndexAxis IndexAxis { get; set; } = IndexAxis.X; + + public BarChartPlugins Plugins { get; set; } = new(); + + public ComboBarLineScales Scales { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLinePlugins.cs b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLinePlugins.cs new file mode 100644 index 0000000..3d76b0a --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLinePlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.ComboBarLineChart; + +public class ComboBarLinePlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineScales.cs b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineScales.cs new file mode 100644 index 0000000..ccd00c0 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ComboBarLineChart/ComboBarLineScales.cs @@ -0,0 +1,16 @@ +using RobotNet.WebApp.Charts.Models.BarChart.Axes; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.ComboBarLineChart; + +public class ComboBarLineScales +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxes? X { get; set; } = new(); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxes? Y { get; set; } = new(); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAxes? Y1 { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/ArcAnimation.cs b/RobotNet.WebApp/Charts/Models/Common/ArcAnimation.cs new file mode 100644 index 0000000..6ce1137 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ArcAnimation.cs @@ -0,0 +1,19 @@ +namespace RobotNet.WebApp.Charts.Models.Common; + +/// +/// The animation-subconfig of the options for a radial chart. +/// +public class ArcAnimation : ChartAnimation +{ + /// + /// Gets or sets a value indicating whether the chart will + /// load in with a rotation animation or not. + /// + public bool? AnimateRotate { get; set; } + + /// + /// Gets or sets a value indicating whether the chart will + /// load in with a scaling animation (from the center outwards) or not. + /// + public bool? AnimateScale { get; set; } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Models/Common/ChartAnimation.cs b/RobotNet.WebApp/Charts/Models/Common/ChartAnimation.cs new file mode 100644 index 0000000..09350d1 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ChartAnimation.cs @@ -0,0 +1,28 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common; + +public class ChartAnimation +{ + /// + /// Gets or sets the number of milliseconds an animation takes. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public long? Duration { get; set; } + + /// + /// Delay before starting the animations. + /// + public int Delay { get; set; } + + /// + /// If set to true, the animations loop endlessly. + /// + public bool Loop { get; set; } + + /// + /// Gets or sets the easing function to use. + /// + public Easing Easing { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/ChartFont.cs b/RobotNet.WebApp/Charts/Models/Common/ChartFont.cs new file mode 100644 index 0000000..18232dc --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ChartFont.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common; + +public class ChartFont +{ + /// + /// Default font family for all text, follows CSS font-family options. + /// 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Family { get; set; } + + /// + /// Height of an individual line of text + /// + public double LineHeight { get; set; } = 1.2; + + /// + /// Default font size (in px) for text. Does not apply to radialLinear scale point labels. + /// + public int Size { get; set; } = 12; + + /// + /// Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. + /// Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Style { get; set; } + + /// + /// Default font weight (boldness). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Weight { get; set; } = "bold"; +} \ No newline at end of file diff --git a/RobotNet.WebApp/Charts/Models/Common/ChartLayout.cs b/RobotNet.WebApp/Charts/Models/Common/ChartLayout.cs new file mode 100644 index 0000000..e9026a6 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ChartLayout.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common; + +public class ChartLayout +{ + /// + /// The padding to add inside the chart + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + + /// + /// Apply automatic padding so visible elements are completely drawn. + /// + public bool AutoPadding { get; set; } = true; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/ChartOptions.cs b/RobotNet.WebApp/Charts/Models/Common/ChartOptions.cs new file mode 100644 index 0000000..6f81f43 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ChartOptions.cs @@ -0,0 +1,67 @@ +using RobotNet.WebApp.Charts.Models.Common.Elements; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common; + +public class ChartOptions : IChartOptions +{ + /// + /// Gets or sets the locale. + /// By default, the chart is using the default locale of the platform which is running on. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Locale { get; set; } + + /// + /// Gets or sets a value indicating whether to maintain the original canvas aspect ratio (width / height) when resizing. + /// + /// + /// Default value is . + /// + public bool MaintainAspectRatio { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the chart canvas should be resized when its container is. + /// + /// + /// Default value is . + /// + public bool Responsive { get; set; } + + /// + /// Gets or sets the canvas aspect ratio (i.e. width / height, a value of 1 representing a square canvas). + /// Note that this option is ignored if the height is explicitly defined either as attribute (of the canvas) or via the style. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? AspectRatio { get; set; } + + /// + /// Gets or sets the duration in milliseconds it takes to animate to new size after a resize event. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ResponsiveAnimationDuration { get; set; } + + /// + /// Gets or sets the animation-configuration for this chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartAnimation? Animation { get; set; } + + /// + /// Gets or sets the layout options for this chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartLayout? Layout { get; set; } + + /// + /// Gets or sets the Interaction configuration for this chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Interaction? Interaction { get; set; } + + /// + /// Gets or sets the elements options for this chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartElements? Elements { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/ChartPadding.cs b/RobotNet.WebApp/Charts/Models/Common/ChartPadding.cs new file mode 100644 index 0000000..9674719 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/ChartPadding.cs @@ -0,0 +1,24 @@ +namespace RobotNet.WebApp.Charts.Models.Common; + +public class ChartPadding +{ + public ChartPadding() { } + public ChartPadding(int padding) + { + Left = padding; + Right = padding; + Top = padding; + Bottom = padding; + } + public ChartPadding(int left, int top, int right, int bottom) + { + Left = left; + Right = top; + Top = right; + Bottom = bottom; + } + public int Left { get; set; } + public int Right { get; set; } + public int Top { get; set; } + public int Bottom { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartData.cs b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartData.cs new file mode 100644 index 0000000..2594748 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartData.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Dataset; + +public class ChartData +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Datasets { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Labels { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDataset.cs b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDataset.cs new file mode 100644 index 0000000..ace94d3 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDataset.cs @@ -0,0 +1,256 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Dataset; + +public class ChartDataset : IChartDataset +{ + public ChartDataset() + { + Oid = Guid.NewGuid(); + } + + /// + /// How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside + /// chartArea. 0 = clip at chartArea. + /// Clipping can also be configured per side: clip: {left: 5, top: false, right: -2, bottom: 0} + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Clip { get; set; } + + /// + /// Get or sets the Data. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Data { get; set; } + + /// + /// Configures the visibility state of the dataset. Set it to , to hide the dataset from the chart. + /// + /// + /// Default value is . + /// + public bool Hidden { get; set; } + + /// + /// The label for the dataset which appears in the legend and tooltips. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Label { get; set; } + + /// + /// Get unique object id. + /// + public Guid Oid { get; private set; } + + /// + /// The drawing order of dataset. Also affects order for stacking, tooltip and legend. + /// + /// + /// Default value is 0. + /// + public int Order { get; set; } + + /// + /// Gets or sets the chart type. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Type { get; set; } + + /// + /// Arc background color. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BackgroundColor { get; set; } + + /// + /// Supported values are 'center' and 'inner'. + /// When 'center' is set, the borders of arcs next to each other will overlap. + /// When 'inner' is set, it is guaranteed that all borders will not overlap. + /// + /// + /// Default value is 'center'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderAlign { get; set; } // TODO: change this to enum + + /// + /// Arc border color. + /// + /// + /// Default value is '#fff'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderColor { get; set; } + + /// + /// Arc border length and spacing of dashes. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderDash { get; set; } + + /// + /// Arc border offset for line dashes. + /// + /// + /// Default value is 0.0. + /// + public double BorderDashOffset { get; set; } + + /// + /// Arc border join style. + /// Supported values are 'round', 'bevel', 'miter'. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderJoinStyle { get; set; } + + /// + /// It is applied to all corners of the arc (outerStart, outerEnd, innerStart, innerRight). + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderRadius { get; set; } + + /// + /// Arc border width (in pixels). + /// + /// + /// Default value is 2. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? BorderWidth { get; set; } + + /// + /// Per-dataset override for the sweep that the arcs cover. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Circumference { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartDatasetDataLabels? Datalabels { get; set; } + + /// + /// Arc background color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBackgroundColor { get; set; } + + /// + /// Arc border color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderColor { get; set; } + + /// + /// Arc border length and spacing of dashes when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderDash { get; set; } + + /// + /// Arc border offset for line dashes when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? HoverBorderDashOffset { get; set; } + + /// + /// Arc border join style when hovered. + /// Supported values are 'round', 'bevel', 'miter'. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderJoinStyle { get; set; } // TODO: change this to enum + + /// + /// Arc border width when hovered (in pixels). + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverBorderWidth { get; set; } + + /// + /// Arc offset when hovered (in pixels). + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? HoverOffset { get; set; } + + /// + /// Arc offset (in pixels). + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? Offset { get; set; } + + /// + /// Per-dataset override for the starting angle to draw arcs from. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Rotation { get; set; } + + /// + /// Fixed arc offset (in pixels). Similar to but applies to all arcs. + /// + /// + /// Default value is 0. + /// + public double Spacing { get; set; } + + /// + /// The relative thickness of the dataset. + /// Providing a value for weight will cause the pie or doughnut dataset to be drawn + /// with a thickness relative to the sum of all the dataset weight values. + /// + /// + /// Default value is 1. + /// + public double Weight { get; set; } = 1; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetData.cs new file mode 100644 index 0000000..38d2336 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetData.cs @@ -0,0 +1,10 @@ +namespace RobotNet.WebApp.Charts.Models.Common.Dataset; + +public class ChartDatasetData(string? datasetLabel, object data, string? backgroundColor) : IChartDatasetData +{ + public string? DatasetLabel { get; init; } = datasetLabel; + + public object Data { get; init; } = data; + + public string? BackgroundColor { get; init; } = backgroundColor; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetDataLabels.cs b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetDataLabels.cs new file mode 100644 index 0000000..7dacaa9 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Dataset/ChartDatasetDataLabels.cs @@ -0,0 +1,24 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Dataset; + +public class ChartDatasetDataLabels +{ + public DataLabelsAlignment Align { get; set; } = DataLabelsAlignment.Center; + public DataLabelsAnchoring Anchor { get; set; } = DataLabelsAnchoring.Center; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + public bool Display { get; set; } = true; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? Font { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } = new(6); +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Elements/Arc.cs b/RobotNet.WebApp/Charts/Models/Common/Elements/Arc.cs new file mode 100644 index 0000000..0b306ec --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Elements/Arc.cs @@ -0,0 +1,55 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Elements; + +public class Arc +{ + /// + /// Arc angle to cover. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Angle { get; set; } + + /// + /// Arc fill color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + + /// + /// Arc stroke alignment. + /// + public BorderAlign BorderAlign { get; set; } = BorderAlign.Center; + + /// + /// Arc stroke color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BorderColor { get; set; } + + /// + /// Arc line dash + /// + public int[]? BorderDash { get; set; } + + /// + /// Arc line dash offset + /// + public double BorderDashOffset { get; set; } + + /// + /// Line join style. + /// + public BorderJoinStyle? BorderJoinStyle { get; set; } + + /// + /// Arc stroke width. + /// + public int BorderWidth { get; set; } = 2; + + /// + /// By default the Arc is curved. If circular: false the Arc will be flat + /// + public bool Circular { get; set; } = true; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Elements/Bar.cs b/RobotNet.WebApp/Charts/Models/Common/Elements/Bar.cs new file mode 100644 index 0000000..cd69b27 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Elements/Bar.cs @@ -0,0 +1,46 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Elements; + +public class Bar +{ + /// + /// Bar fill color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + + /// + /// Bar stroke width. + /// + public int BorderWidth { get; set; } = 1; + + /// + /// Bar stroke color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BorderColor { get; set; } + + /// + /// Skipped (excluded) border. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BorderSkipped? BorderSkipped { get; set; } + + /// + /// The bar border radius (in pixels). + /// + public int BorderRadius { get; set; } + + /// + /// The amount of pixels to inflate the bar rectangle(s) when drawing. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? InflateAmount { get; set; } + + /// + /// Style of the point for legend. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PointStyle? PointStyle { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Elements/ChartElements.cs b/RobotNet.WebApp/Charts/Models/Common/Elements/ChartElements.cs new file mode 100644 index 0000000..af2c1ec --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Elements/ChartElements.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Elements; + +public class ChartElements +{ + /// + /// Arcs are used in the polar area, doughnut and pie charts. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Arc? Arc { get; set; } + + /// + /// Bar elements are used to represent the bars in a bar chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Bar? Bar { get; set; } + + /// + /// Line elements are used to represent the line in a line chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Line? Line { get; set; } + + /// + /// Point elements are used to represent the points in a line, radar or bubble chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Point? Point { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Elements/Line.cs b/RobotNet.WebApp/Charts/Models/Common/Elements/Line.cs new file mode 100644 index 0000000..2ce3c80 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Elements/Line.cs @@ -0,0 +1,71 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Elements; + +public class Line +{ + /// + /// Bézier curve tension (0 for no Bézier curves). + /// + public int Tension { get; set; } + + /// + /// Line fill color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + + /// + /// Line stroke width. + /// + public int BorderWidth { get; set; } = 1; + + /// + /// Line stroke color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BorderColor { get; set; } + + /// + /// Line cap style. + /// + public string BorderCapStyle { get; set; } = "butt"; + + /// + /// Line dash. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int[]? BorderDash { get; set; } + + /// + /// Line dash offset. + /// + public double BorderDashOffset { get; set; } + + /// + /// Line join style. + /// + public BorderJoinStyle BorderJoinStyle { get; set; } = BorderJoinStyle.Miter; + + /// + /// if true to keep Bézier control inside the chart, false for no restriction. + /// + public bool CapBezierPoints { get; set; } = true; + + /// + /// Interpolation mode to apply. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? CubicInterpolationMode { get; set; } + + /// + /// How to fill the area under the line. + /// + public bool Fill { get; set; } + + /// + /// if true to show the line as a stepped line (tension will be ignored). + /// + public bool Stepped { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Elements/Point.cs b/RobotNet.WebApp/Charts/Models/Common/Elements/Point.cs new file mode 100644 index 0000000..5a6f62c --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Elements/Point.cs @@ -0,0 +1,54 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Elements; + +public class Point +{ + /// + /// Point radius. + /// + public double Radius { get; set; } = 3; + + /// + /// Point style. + /// + public PointStyle PointStyle { get; set; } = PointStyle.Circle; + + /// + /// Point rotation (in degrees). + /// + public double Rotation { get; set; } + + /// + /// Point fill color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + + /// + /// Point stroke width. + /// + public int BorderWidth { get; set; } = 1; + + /// + /// Point stroke color. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BorderColor { get; set; } + + /// + /// Extra radius added to point radius for hit detection. + /// + public double HitRadius { get; set; } = 1; + + /// + /// Point radius when hovered. + /// + public int HhoverRadius { get; set; } = 4; + + /// + /// Stroke width when hovered. + /// + public int HoverBorderWidth { get; set; } = 1; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/IChartDataset.cs b/RobotNet.WebApp/Charts/Models/Common/IChartDataset.cs new file mode 100644 index 0000000..358475d --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/IChartDataset.cs @@ -0,0 +1,5 @@ +namespace RobotNet.WebApp.Charts.Models.Common; + +public interface IChartDataset +{ +} diff --git a/RobotNet.WebApp/Charts/Models/Common/IChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/Common/IChartDatasetData.cs new file mode 100644 index 0000000..27c5695 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/IChartDatasetData.cs @@ -0,0 +1,5 @@ +namespace RobotNet.WebApp.Charts.Models.Common; + +public interface IChartDatasetData +{ +} diff --git a/RobotNet.WebApp/Charts/Models/Common/IChartOptions.cs b/RobotNet.WebApp/Charts/Models/Common/IChartOptions.cs new file mode 100644 index 0000000..aadfd03 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/IChartOptions.cs @@ -0,0 +1,5 @@ +namespace RobotNet.WebApp.Charts.Models.Common; + +public interface IChartOptions +{ +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Interaction.cs b/RobotNet.WebApp/Charts/Models/Common/Interaction.cs new file mode 100644 index 0000000..281618e --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Interaction.cs @@ -0,0 +1,67 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common; + +public class Interaction +{ + private InteractionMode mode; + + public Interaction() => Mode = InteractionMode.Nearest; + + private void SetMode(InteractionMode interactionMode) => + ChartInteractionMode = interactionMode switch + { + InteractionMode.Dataset => "dataset", + InteractionMode.Index => "index", + InteractionMode.Nearest => "nearest", + InteractionMode.Point => "point", + InteractionMode.X => "x", + InteractionMode.Y => "y", + _ => "" + }; + + + /// + /// Sets which elements appear in the interaction. + /// + [JsonPropertyName("mode")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ChartInteractionMode { get; private set; } + + /// + /// if , the interaction mode only applies when the mouse position intersects an item on the chart. + /// + /// + /// Default value is . + /// + public bool Intersect { get; set; } = true; + + /// + /// Sets which elements appear in the tooltip. See Interaction Modes for details. + /// + [JsonIgnore] + public InteractionMode Mode + { + get => mode; + set + { + mode = value; + SetMode(value); + } + } + + /// + /// Gets or sets which directions are used in calculating distances. + /// Defaults to for == + /// and for == or . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AxisDirection? Axis { get; set; } + + /// + /// if true, the invisible points that are outside of the chart area will also be included when evaluating interactions. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? IncludeInvisible { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPlugins.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPlugins.cs new file mode 100644 index 0000000..a885e3e --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPlugins.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPlugins +{ + /// + /// The chart legend displays data about the datasets that are appearing on the chart. + /// + public ChartPluginsLegend Legend { get; set; } = new(); + + /// + /// The chart title defines text to draw at the top of the chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsTitle? Title { get; set; } + + /// + /// Tooltip for the element. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsTooltip? Tooltip { get; set; } + + /// + /// DataLabels for the element. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsDataLabels? Datalabels { get; set; } + + /// + /// Subtitle for the element. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsSubtitle? Subtitle { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsDataLabels.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsDataLabels.cs new file mode 100644 index 0000000..5ebfbbc --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsDataLabels.cs @@ -0,0 +1,37 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsDataLabels +{ + public DataLabelsAlignment Align { get; set; } = DataLabelsAlignment.Center; + public DataLabelsAnchoring Anchor { get; set; } = DataLabelsAnchoring.Center; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BackgroundColor { get; set; } + public string BorderColor { get; set; } = "white"; + public double BorderRadius { get; set; } = 25; + public double BorderWidth { get; set; } = 2; + public bool Clamp { get; set; } = false; + public bool Clip { get; set; } = false; + public string Color { get; set; } = "white"; + public bool Display { get; set; } = true; + public ChartFont Font { get; set; } = new(); + public int Offset { get; set; } = 4; + public double Opacity { get; set; } = 1; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + public int Rotation { get; set; } + + public DataLabelsAlignment TextAlign { get; set; } = DataLabelsAlignment.Center; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TextStrokeColor { get; set; } + public int TextStrokeWidth { get; set; } + public int TextShadowBlur { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TextShadowColor { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegend.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegend.cs new file mode 100644 index 0000000..4310715 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegend.cs @@ -0,0 +1,67 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsLegend +{ + /// + /// Alignment of the legend. Default values is 'center'. Other possible values 'start' and 'end'. + /// + public LegendAlignment Align { get; set; } = LegendAlignment.Center; + + /// + /// Is the legend shown? Default value is 'true'. + /// + public bool Display { get; set; } = true; + + /// + /// If , Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. + /// + public bool FullSize { get; set; } = true; + + /// + /// Label settings for the legend. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsLegendLabels? Labels { get; set; } + + /// + /// Maximum height of the legend, in pixels. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxHeight { get; set; } + + /// + /// Maximum width of the legend, in pixels. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxWidth { get; set; } + + /// + /// Position of the legend. Default value is 'top'. Other possible value is 'bottom'. + /// + public LegendPosition Position { get; set; } = LegendPosition.Top; + + /// + /// If , the Legend will show datasets in reverse order. + /// + public bool Reverse { get; set; } = false; + + /// + /// If , for rendering of the legends will go from right to left. + /// + public bool Rtl { get; set; } = false; + + /// + /// This will force the text direction 'rtl' or 'ltr' on the canvas for rendering the legend, regardless of the css specified on the canvas + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TextDirection { get; set; } + + /// + /// Title object + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPluginsLegendTitle? Title { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendLabels.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendLabels.cs new file mode 100644 index 0000000..5d1ef99 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendLabels.cs @@ -0,0 +1,67 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsLegendLabels +{ + /// + /// Width of coloured box. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BoxWidth { get; set; } + + /// + /// Height of the coloured box + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BoxHeight { get; set; } + + /// + /// Override the borderRadius to use. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BorderRadius { get; set; } + + /// + /// Color of label and the strikethrough. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? Font { get; set; } + + /// + /// Padding between rows of colored boxes. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Padding { get; set; } + + /// + /// If specified, this style of point is used for the legend. Only used if > is true. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PointStyle? PointStyle { get; set; } + + /// + /// If is , the width of the point style used for the legend. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? PointStyleWidth { get; set; } + + /// + /// Label borderRadius will match corresponding . + /// + public bool UseBorderRadius { get; set; } = false; + + /// + /// If , Label style will match corresponding point style (size is based on pointStyleWidth or the minimum value between and -> Size). + /// + public bool UsePointStyle { get; set; } = false; + + /// + /// Horizontal alignment of the label text + /// + public LegendAlignment TextAlign { get; set; } = LegendAlignment.Center; +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendTitle.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendTitle.cs new file mode 100644 index 0000000..6ea1634 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsLegendTitle.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsLegendTitle +{ + /// + /// Color of the legend. Default value is 'black'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Is the legend title displayed. + /// + public bool Display { get; set; } = true; + + /// + /// Padding around the title. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + + /// + /// The string title + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } + + /// + /// Font Legend Title + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? Font { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsSubtitle.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsSubtitle.cs new file mode 100644 index 0000000..a55ed37 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsSubtitle.cs @@ -0,0 +1,42 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsSubtitle +{ + /// + /// Alignment of the title. + /// Options are: 'start', 'center', and 'end' + /// + public TitleAlignment Align { get; set; } = TitleAlignment.Center; + /// + /// Color of text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Is the title shown? + /// + public bool Display { get; set; } = true; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? Font { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } + + public bool FullSize { get; set; } = true; + + /// + /// Gets or sets the position of the title. + /// + public TitlePosition Position { get; set; } = TitlePosition.Top; + + /// + /// Gets or sets the number of pixels to add above and below the title text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTitle.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTitle.cs new file mode 100644 index 0000000..76ecfef --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTitle.cs @@ -0,0 +1,42 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsTitle +{ + /// + /// Alignment of the title. + /// Options are: 'start', 'center', and 'end' + /// + public TitleAlignment Align { get; set; } = TitleAlignment.Center; + /// + /// Color of text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Is the title shown? + /// + public bool Display { get; set; } = true; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? Font { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Text { get; set; } + + public bool FullSize { get; set; } = true; + + /// + /// Gets or sets the position of the title. + /// + public TitlePosition Position { get; set; } = TitlePosition.Top; + + /// + /// Gets or sets the number of pixels to add above and below the title text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTooltip.cs b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTooltip.cs new file mode 100644 index 0000000..5134605 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/Common/Plugins/ChartPluginsTooltip.cs @@ -0,0 +1,176 @@ +using RobotNet.WebApp.Charts.Enums; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.Common.Plugins; + +public class ChartPluginsTooltip +{ + /// + /// Background color of the tooltip. + /// + public string BackgroundColor { get; set; } = "rgba(0, 0, 0, 0.8)"; + + /// + /// Gets or sets which elements appear in the tooltip. + /// + public InteractionMode Mode { get; set; } + + /// + /// Gets or sets the value indicating if the hover mode only applies when the mouse position intersects an item on the chart. + /// + public bool? Intersect { get; set; } + + /// + /// The mode for positioning the tooltip. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TooltipPosition? Position { get; set; } = TooltipPosition.Average; + + /// + /// Horizontal alignment of the body text lines. left/right/center. + /// + public TooltipXAlignment BodyAlign { get; set; } = TooltipXAlignment.Left; + + /// + /// Color of body text. + /// + public string BodyColor { get; set; } = "#fff"; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartFont? BodyFont { get; set; } + + /// + /// Spacing to add to top and bottom of each tooltip item. + /// + public int BodySpacing { get; set; } = 2; + + /// + /// Padding for Tooltips + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ChartPadding? Padding { get; set; } + + /// + /// Extra distance to move the end of the tooltip arrow away from the tooltip point. + /// + public int CaretPadding { get; set; } = 2; + + /// + /// Size, in px, of the tooltip arrow. + /// + public int CaretSize { get; set; } = 5; + + /// + /// If , color boxes are shown in the tooltip. + /// + public bool DisplayColors { get; set; } = true; + + /// + /// Are on-canvas tooltips enabled? + /// + public bool Enabled { get; set; } = true; + + /// + /// Horizontal alignment of the footer text lines. left/right/center. + /// + public TooltipXAlignment FooterAlign { get; set; } = TooltipXAlignment.Left; + + /// + /// Color of footer text. + /// + public string FooterColor { get; set; } = "#fff"; + + public ChartFont FooterFont { get; set; } = new(); + + /// + /// Margin to add before drawing the footer. + /// + public int FooterMarginTop { get; set; } = 6; + + /// + /// Spacing to add to top and bottom of each footer line. + /// + public int FooterSpacing { get; set; } = 2; + + /// + /// Horizontal alignment of the title text lines. left/right/center. + /// + public TooltipXAlignment TitleAlign { get; set; } = TooltipXAlignment.Left; + + /// + /// Color of title text. + /// + public string TitleColor { get; set; } = "#fff"; + + public ChartFont TitleFont { get; set; } = new(); + + /// + /// Margin to add on bottom of title section. + /// + public int TitleMarginBottom { get; set; } = 6; + + /// + /// Spacing to add to top and bottom of each title line. + /// + public int TitleSpacing { get; set; } = 2; + + /// + /// Position of the tooltip caret in the X direction. left/center/right. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TooltipXAlignment? XAlign { get; set; } + + /// + /// Position of the tooltip caret in the Y direction. top/center/bottom. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public TooltipYAlignment? YAlign { get; set; } + + /// + /// Width of the color box if displayColors is true. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BoxWidth { get; set; } + + /// + /// Height of the color box if displayColors is true. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BoxHeight { get; set; } + + /// + /// Padding between the color box and the text. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BoxPadding { get; set; } + + /// + /// Size of the border. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BorderWidth { get; set; } + + /// + /// Color of the border. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? BorderColor { get; set; } + + /// + /// Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? UsePointStyle { get; set; } = false; + + /// + /// true for rendering the tooltip from right to left. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Rtl { get; set; } + + /// + /// This will force the text direction 'rtl' or 'ltr' on the canvas for rendering the tooltips, regardless of the css specified on the canvas + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? TextDirection { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/LineChart/LineChartDataset.cs b/RobotNet.WebApp/Charts/Models/LineChart/LineChartDataset.cs new file mode 100644 index 0000000..cfe1c13 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/LineChart/LineChartDataset.cs @@ -0,0 +1,216 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.LineChart; + +public class LineChartDataset : ChartDataset +{ + + /// + /// Cap style of the line. + /// Supported values are 'butt', 'round', and 'square'. + /// + /// + /// Default value is 'butt'. + /// + public string BorderCapStyle { get; set; } = "butt"; + + /// + /// Draw the active points of a dataset over the other points of the dataset. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? DrawActiveElementsOnTop { get; set; } + + /// + /// How to fill the area under the line. + /// + /// + /// Default value is . + /// + public object Fill { get; set; } = false; + + /// + /// Cap style of the line when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? HoverBorderCapStyle { get; set; } + + /// + /// The base axis of the dataset. 'x' for horizontal lines and 'y' for vertical lines. + /// + /// + /// Default value is 'x'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IndexAxis? IndexAxis { get; set; } + + /// + /// The fill color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBackgroundColor { get; set; } + + /// + /// The border color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderColor { get; set; } + + /// + /// The width of the point border in pixels. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderWidth { get; set; } + + /// + /// The pixel size of the non-displayed point that reacts to mouse events. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHitRadius { get; set; } + + /// + /// Point background color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBackgroundColor { get; set; } + + /// + /// Point border color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderColor { get; set; } + + /// + /// Border width of point when hovered. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderWidth { get; set; } + + /// + /// The radius of the point when hovered. + /// + /// + /// Default value is 4. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverRadius { get; set; } + + /// + /// The radius of the point shape. If set to 0, the point is not rendered. + /// + /// + /// Default value is 3. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRadius { get; set; } + + /// + /// The rotation of the point in degrees. + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRotation { get; set; } + + /// + /// Style of the point. + /// Supported values are 'circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', and + /// 'triangle' to style. + /// the point. + /// + /// + /// Default value is 'circle'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointStyle { get; set; } + + //segment + //https://www.chartjs.org/docs/latest/charts/line.html#segment + + /// + /// If , the lines between points are not drawn. + /// + /// + /// Default value is . + /// + public bool ShowLine { get; set; } = true; + + /// + /// If , lines will be drawn between points with no or null data. + /// If , points with null data will create a break in the line. + /// Can also be a number specifying the maximum gap length to span. + /// The unit of the value depends on the scale used. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? SpanGaps { get; set; } + + //stack + //https://www.chartjs.org/docs/latest/charts/line.html#general + + /// + /// true to show the line as a stepped line (tension will be ignored). + /// + /// + /// Default value is . + /// + public bool Stepped { get; set; } + + /// + /// Bezier curve tension of the line. Set to 0 to draw straight lines. + /// This option is ignored if monotone cubic interpolation is used. + /// + /// + /// Default value is 0. + /// + public double Tension { get; set; } + + /// + /// The ID of the x axis to plot this dataset on. + /// + /// + /// Default value is 'first x axis'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? XAxisID { get; set; } + + /// + /// The ID of the y axis to plot this dataset on. + /// + /// + /// Default value is 'first y axis'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? YAxisID { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/LineChart/LineChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/LineChart/LineChartDatasetData.cs new file mode 100644 index 0000000..18d0fea --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/LineChart/LineChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.LineChart; + +public class LineChartDatasetData(string? datasetLabel, object data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/LineChart/LineChartOptions.cs b/RobotNet.WebApp/Charts/Models/LineChart/LineChartOptions.cs new file mode 100644 index 0000000..1e5d439 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/LineChart/LineChartOptions.cs @@ -0,0 +1,21 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BarChart; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.LineChart; + +public class LineChartOptions : ChartOptions +{ + /// + /// The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + /// Supported values are 'x' and 'y'. + /// + /// + /// Default value is . + /// + public IndexAxis IndexAxis { get; set; } = IndexAxis.X; + + public LineChartPlugins Plugins { get; set; } = new(); + + public BarScales Scales { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/LineChart/LineChartPlugins.cs b/RobotNet.WebApp/Charts/Models/LineChart/LineChartPlugins.cs new file mode 100644 index 0000000..0b2c507 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/LineChart/LineChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.LineChart; + +public class LineChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/PieChart/PieChartDataset.cs b/RobotNet.WebApp/Charts/Models/PieChart/PieChartDataset.cs new file mode 100644 index 0000000..067da73 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PieChart/PieChartDataset.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.PieChart; + +public class PieChartDataset : ChartDataset +{ +} diff --git a/RobotNet.WebApp/Charts/Models/PieChart/PieChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/PieChart/PieChartDatasetData.cs new file mode 100644 index 0000000..94d8eca --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PieChart/PieChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.PieChart; + +public class PieChartDatasetData(string? datasetLabel, double data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/PieChart/PieChartOptions.cs b/RobotNet.WebApp/Charts/Models/PieChart/PieChartOptions.cs new file mode 100644 index 0000000..dfe1361 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PieChart/PieChartOptions.cs @@ -0,0 +1,40 @@ +using RobotNet.WebApp.Charts.Models.Common; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.PieChart; + +public class PieChartOptions : ChartOptions +{ + public PieChartPlugins Plugins { get; set; } = new(); + + /// + /// Gets or sets the percentage of the chart that is cut out of the middle. + /// Default for Pie is 0, Default for Doughnut is 50. This will be filled in by Chart.js unless you specify a non-null value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Cutout { get; set; } + + /// + /// Gets or sets the animation-configuration for this chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public new ArcAnimation? Animation { get; set; } + + /// + /// Gets or sets the starting angle to draw arcs from. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Rotation { get; set; } + + /// + /// The outer radius of the chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Radius { get; set; } + + /// + /// Gets or sets the sweep to allow arcs to cover. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Circumference { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/PieChart/PieChartPlugins.cs b/RobotNet.WebApp/Charts/Models/PieChart/PieChartPlugins.cs new file mode 100644 index 0000000..d28e506 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PieChart/PieChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.PieChart; + +public class PieChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDataset.cs b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDataset.cs new file mode 100644 index 0000000..6a6980d --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDataset.cs @@ -0,0 +1,8 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.PolarAreaChart; + +public class PolarAreaChartDataset : ChartDataset +{ + public bool Circular { get; set; } = true; +} diff --git a/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDatasetData.cs new file mode 100644 index 0000000..a641c41 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.PolarAreaChart; + +public class PolarAreaChartDatasetData(string? datasetLabel, double data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartOptions.cs b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartOptions.cs new file mode 100644 index 0000000..fb73e4d --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartOptions.cs @@ -0,0 +1,12 @@ +using RobotNet.WebApp.Charts.Models.Common; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.PolarAreaChart; + +public class PolarAreaChartOptions : ChartOptions +{ + public PolarAreaChartPlugins Plugins { get; set; } = new(); + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public new ArcAnimation? Animation { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartPlugins.cs b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartPlugins.cs new file mode 100644 index 0000000..a53fd07 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/PolarAreaChart/PolarAreaChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.PolarAreaChart; + +public class PolarAreaChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/AngleLines.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/AngleLines.cs new file mode 100644 index 0000000..9261706 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/AngleLines.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class AngleLines +{ + /// + /// Gets or sets the value indicating whether the angle line is displayed or not. + /// + public bool Display { get; set; } = true; + + /// + /// Gets or sets the color of the angled lines. + /// See for working with colors. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Gets or sets the width of the angled lines. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? LineWidth { get; set; } + + /// + /// Gets or sets the length and spacing of dashes of the angled lines. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int[]? BorderDash { get; set; } + + /// + /// Gets or sets the offset for line dashes. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? BorderDashOffset { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/GridLines.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/GridLines.cs new file mode 100644 index 0000000..7d4a462 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/GridLines.cs @@ -0,0 +1,90 @@ +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class GridLines +{ + /// + /// If false, do not display grid lines for this axis. + /// + public bool Display { get; set; } = true; + + /// + /// If true, gridlines are circular (on radar chart only). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? Circular { get; set; } + + /// + /// The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line and so on. + /// See for working with colors. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Color { get; set; } + + /// + /// Length and spacing of dashes on grid lines + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double[]? BorderDash { get; set; } + + /// + /// Stroke width of grid lines. + /// + public double LineWidth { get; set; } = 1; + + /// + /// If true, draw border at the edge between the axis and the chart area. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DrawBorder { get; set; } + + /// + /// If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DrawOnChartArea { get; set; } + + /// + /// If true, draw lines beside the ticks in the axis area beside the chart. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? DrawTicks { get; set; } + + /// + /// Length in pixels that the grid lines will draw into the axis area. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TickMarkLength { get; set; } + + /// + /// Stroke width of the grid line for the first index (index 0). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? ZeroLineWidth { get; set; } + + /// + /// Stroke color of the grid line for the first index (index 0). + /// See for working with colors. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ZeroLineColor { get; set; } + + /// + /// Length and spacing of dashes of the grid line for the first index (index 0). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double[]? ZeroLineBorderDash { get; set; } + + /// + /// Offset for line dashes of the grid line for the first index (index 0). + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? ZeroLineBorderDashOffset { get; set; } + + /// + /// If true, grid lines will be shifted to be between labels. This is set to true for a category scale in a bar chart by default. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? OffsetGridLines { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/PointLabels.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/PointLabels.cs new file mode 100644 index 0000000..04ffbc9 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/PointLabels.cs @@ -0,0 +1,47 @@ +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class PointLabels +{ + /// + /// Gets or sets the font color for a point label. + /// See for working with colors. + /// + public string? Color { get; set; } + + /// + /// If true, point labels are shown. When display: 'auto', the label is hidden if it overlaps with another label + /// + public bool Display { get; set; } + + /// + /// Background color of the point label. + /// + public string? BackdropColor { get; set; } + + /// + /// Border radius of the point label + /// + public double? BorderRadius { get; set; } + + /// + /// Padding of label backdrop. + /// + public ChartPadding? BackdropPadding { get; set; } + + /// + /// Padding between chart and point labels. + /// + public ChartPadding? Padding { get; set; } + + /// + /// Font of the point label + /// + public ChartFont? Font { get; set; } + + /// + /// If true, point labels are centered. + /// + public bool? CenterPointLabels { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarAxis.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarAxis.cs new file mode 100644 index 0000000..aaffb0f --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarAxis.cs @@ -0,0 +1,41 @@ +using RobotNet.WebApp.Charts.Models.BarChart.Axes; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class RadarAxis : ChartAxes +{ + /// + /// Gets or sets the angle lines configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public AngleLines? AngleLines { get; set; } + + /// + /// Gets or sets the grid lines configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public GridLines? GridLines { get; set; } + + /// + /// Gets or sets the point labels configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public PointLabels? PointLabels { get; set; } + + /// + /// Gets or sets the ticks configuration. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public new RadialTicks? Ticks { get; set; } + + /// + /// Whether to animate scaling the chart from the centre + /// + public bool Animate { get; set; } = true; + + /// + /// Starting angle of the scale. In degrees, 0 is at top + /// + public double StartAngle { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarScale.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarScale.cs new file mode 100644 index 0000000..2bb7a98 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadarScale.cs @@ -0,0 +1,6 @@ +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class RadarScale +{ + public RadarAxis X { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadialTicks.cs b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadialTicks.cs new file mode 100644 index 0000000..db559ef --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/Axis/RadialTicks.cs @@ -0,0 +1,67 @@ +using RobotNet.WebApp.Charts.Models.BarChart.Axes; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.RadarChart.Axis; + +public class RadialTicks : ChartAxesTicks +{ + /// + /// Gets or sets the horizontal padding of label backdrop. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BackdropPaddingX { get; set; } + + /// + /// Gets or sets the vertical padding of label backdrop. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? BackdropPaddingY { get; set; } + + /// + /// Gets or sets the value indicating whether the scale will include 0 if it is not already included. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? BeginAtZero { get; set; } + + /// + /// Gets or sets the user defined minimum number for the scale, overrides minimum value from data. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Min { get; set; } + + /// + /// Gets or sets the user defined maximum number for the scale, overrides minimum value from data. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Max { get; set; } + + /// + /// Gets or sets the maximum number of ticks and gridlines to show. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxTicksLimit { get; set; } + + /// + /// If defined and is not specified, the step size will be rounded to this many decimal places. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? Precision { get; set; } + + /// + /// Gets or sets the user defined fixed step size for the scale. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? StepSize { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the maximum data value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? SuggestedMax { get; set; } + + /// + /// Gets or sets the adjustment used when calculating the minimum data value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? SuggestedMin { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDataset.cs b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDataset.cs new file mode 100644 index 0000000..5162908 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDataset.cs @@ -0,0 +1,156 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.RadarChart; + +public class RadarChartDataset : ChartDataset +{ + /// + /// Cap style of the line. + /// Supported values are 'butt', 'round', and 'square'. + /// + /// + /// Default value is 'butt'. + /// + public string BorderCapStyle { get; set; } = "butt"; + + /// + /// How to fill the area under the line. + /// + /// + /// Default value is . + /// + public bool Fill { get; set; } + + /// + /// Cap style of the line when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? HoverBorderCapStyle { get; set; } + + /// + /// The fill color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBackgroundColor { get; set; } + + /// + /// The border color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderColor { get; set; } + + /// + /// The width of the point border in pixels. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderWidth { get; set; } + + /// + /// The pixel size of the non-displayed point that reacts to mouse events. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHitRadius { get; set; } + + /// + /// Point background color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBackgroundColor { get; set; } + + /// + /// Point border color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderColor { get; set; } + + /// + /// Border width of point when hovered. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderWidth { get; set; } + + /// + /// The radius of the point when hovered. + /// + /// + /// Default value is 4. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverRadius { get; set; } + + /// + /// The radius of the point shape. If set to 0, the point is not rendered. + /// + /// + /// Default value is 3. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRadius { get; set; } + + /// + /// The rotation of the point in degrees. + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRotation { get; set; } + + /// + /// Style of the point. + /// Supported values are 'circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', and + /// 'triangle' to style. + /// the point. + /// + /// + /// Default value is 'circle'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointStyle { get; set; } + + /// + /// If , lines will be drawn between points with no or null data. + /// If , points with null data will create a break in the line. + /// Can also be a number specifying the maximum gap length to span. + /// The unit of the value depends on the scale used. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? SpanGaps { get; set; } + + /// + /// Bezier curve tension of the line. Set to 0 to draw straight lines. + /// This option is ignored if monotone cubic interpolation is used. + /// + /// + /// Default value is 0. + /// + public double Tension { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDatasetData.cs new file mode 100644 index 0000000..75b2359 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.RadarChart; + +public class RadarChartDatasetData(string? datasetLabel, double data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartOptions.cs b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartOptions.cs new file mode 100644 index 0000000..74d6148 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartOptions.cs @@ -0,0 +1,20 @@ +using RobotNet.WebApp.Charts.Models.RadarChart.Axis; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.RadarChart; + +public class RadarChartOptions : ChartOptions +{ + /// + /// Gets or sets the scale configuration for this chart. + /// + public RadarScale Scales { get; set; } = new(); + + /// + /// Gets or sets a value indicating whether or not line gaps (by NaN data) will be spanned. + /// If , NaN data causes a break in the line. + /// + public bool? SpanGaps { get; set; } + + public RadarChartPlugins Plugins { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartPlugins.cs b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartPlugins.cs new file mode 100644 index 0000000..fdc85ee --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/RadarChart/RadarChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.RadarChart; + +public class RadarChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataPoint.cs b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataPoint.cs new file mode 100644 index 0000000..102fbd4 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataPoint.cs @@ -0,0 +1,4 @@ +namespace RobotNet.WebApp.Charts.Models.ScatterChart; + +public record ScatterChartDataPoint(double X, double Y); + diff --git a/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataset.cs b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataset.cs new file mode 100644 index 0000000..31f20d5 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDataset.cs @@ -0,0 +1,222 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; +using System.Text.Json.Serialization; + +namespace RobotNet.WebApp.Charts.Models.ScatterChart; + +public class ScatterChartDataset : ChartDataset +{ + /// + /// Cap style of the line. + /// Supported values are 'butt', 'round', and 'square'. + /// + /// + /// Default value is 'butt'. + /// + public string BorderCapStyle { get; set; } = "butt"; + + + + /// + /// Supported values are 'default', and 'monotone'. + /// + /// + /// Default value is 'default'. + /// + public string CubicInterpolationMode { get; set; } = "default"; + + /// + /// Draw the active points of a dataset over the other points of the dataset. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? DrawActiveElementsOnTop { get; set; } + + /// + /// How to fill the area under the line. + /// + /// + /// Default value is . + /// + public bool Fill { get; set; } + + /// + /// Cap style of the line when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? HoverBorderCapStyle { get; set; } + + /// + /// The base axis of the dataset. 'x' for horizontal lines and 'y' for vertical lines. + /// + /// + /// Default value is 'x'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? IndexAxis { get; set; } + + /// + /// The fill color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBackgroundColor { get; set; } + + /// + /// The border color for points. + /// + /// + /// Default value is 'rgba(0, 0, 0, 0.1)'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderColor { get; set; } + + /// + /// The width of the point border in pixels. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointBorderWidth { get; set; } + + /// + /// The pixel size of the non-displayed point that reacts to mouse events. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHitRadius { get; set; } + + /// + /// Point background color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBackgroundColor { get; set; } + + /// + /// Point border color when hovered. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderColor { get; set; } + + /// + /// Border width of point when hovered. + /// + /// + /// Default value is 1. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverBorderWidth { get; set; } + + /// + /// The radius of the point when hovered. + /// + /// + /// Default value is 4. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointHoverRadius { get; set; } + + /// + /// The radius of the point shape. If set to 0, the point is not rendered. + /// + /// + /// Default value is 3. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRadius { get; set; } + + /// + /// The rotation of the point in degrees. + /// + /// + /// Default value is 0. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointRotation { get; set; } + + /// + /// Style of the point. + /// Supported values are 'circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', and + /// 'triangle' to style. + /// the point. + /// + /// + /// Default value is 'circle'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? PointStyle { get; set; } + + //segment + //https://www.chartjs.org/docs/latest/charts/line.html#segment + + /// + /// If , the lines between points are not drawn. + /// By default, the scatter chart will override the showLine property of the line chart to false. + /// + /// + /// Default value is . + /// + public bool ShowLine { get; } = false; + + /// + /// If , lines will be drawn between points with no or null data. + /// If , points with null data will create a break in the line. + /// Can also be a number specifying the maximum gap length to span. + /// The unit of the value depends on the scale used. + /// + /// + /// Default value is . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? SpanGaps { get; set; } + + /// + /// true to show the line as a stepped line (tension will be ignored). + /// + /// + /// Default value is . + /// + public bool Stepped { get; set; } + + /// + /// Bezier curve tension of the line. Set to 0 to draw straight lines. + /// This option is ignored if monotone cubic interpolation is used. + /// + /// + /// Default value is 0. + /// + public double Tension { get; set; } + + /// + /// The ID of the x axis to plot this dataset on. + /// + /// + /// Default value is 'first x axis'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? XAxisID { get; set; } + + /// + /// The ID of the y axis to plot this dataset on. + /// + /// + /// Default value is 'first y axis'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? YAxisID { get; set; } +} diff --git a/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDatasetData.cs b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDatasetData.cs new file mode 100644 index 0000000..b4f5195 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartDatasetData.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Dataset; + +namespace RobotNet.WebApp.Charts.Models.ScatterChart; + +public class ScatterChartDatasetData(string? datasetLabel, double data, string? backgroundColor) : ChartDatasetData(datasetLabel, data, backgroundColor) +{ +} diff --git a/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartOptions.cs b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartOptions.cs new file mode 100644 index 0000000..8151132 --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartOptions.cs @@ -0,0 +1,22 @@ +using RobotNet.WebApp.Charts.Enums; +using RobotNet.WebApp.Charts.Models.BarChart; +using System.Text.Json.Serialization; +using RobotNet.WebApp.Charts.Models.Common; + +namespace RobotNet.WebApp.Charts.Models.ScatterChart; + +public class ScatterChartOptions : ChartOptions +{ + /// + /// The base axis of the dataset. 'x' for horizontal lines and 'y' for vertical lines. + /// + /// + /// Default value is 'x'. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IndexAxis? IndexAxis { get; set; } + + public ScatterChartPlugins Plugins { get; set; } = new(); + + public BarScales Scales { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartPlugins.cs b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartPlugins.cs new file mode 100644 index 0000000..fb5c66f --- /dev/null +++ b/RobotNet.WebApp/Charts/Models/ScatterChart/ScatterChartPlugins.cs @@ -0,0 +1,7 @@ +using RobotNet.WebApp.Charts.Models.Common.Plugins; + +namespace RobotNet.WebApp.Charts.Models.ScatterChart; + +public class ScatterChartPlugins : ChartPlugins +{ +} diff --git a/RobotNet.WebApp/Clients/ConsoleHubClient.cs b/RobotNet.WebApp/Clients/ConsoleHubClient.cs new file mode 100644 index 0000000..24ef61a --- /dev/null +++ b/RobotNet.WebApp/Clients/ConsoleHubClient.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; + +namespace RobotNet.WebApp.Clients; + +public class ConsoleHubClient : WebAssemblyHubClient +{ + public event Action? MessageInfo; + private IDisposable? disMessageInfo; + + public event Action? MessageWarning; + private IDisposable? disMessageWarning; + + public event Action? MessageError; + private IDisposable? disMessageError; + + public ConsoleHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + disMessageInfo = Connection.On(nameof(MessageInfo), message => MessageInfo?.Invoke(message)); + disMessageWarning = Connection.On(nameof(MessageWarning), message => MessageWarning?.Invoke(message)); + disMessageError = Connection.On(nameof(MessageError), message => MessageError?.Invoke(message)); + } + + public override async Task StopAsync() + { + disMessageInfo?.Dispose(); + disMessageInfo = null; + disMessageWarning?.Dispose(); + disMessageWarning = null; + disMessageError?.Dispose(); + disMessageError = null; + await base.StopAsync(); + } + + public Task RegisterTaskConsole(string name) => Connection.InvokeAsync("RegisterTasksConsole", name); + public Task UnregisterTaskConsole(string name) => Connection.InvokeAsync("UnregisterTasksConsole", name); + public Task RegisterTaskConsoles() => Connection.InvokeAsync("RegisterTaskConsoles"); + public Task UnregisterTaskConsoles() => Connection.InvokeAsync("UnregisterTaskConsoles"); + public Task RegisterMissionConsole(Guid missionId) => Connection.InvokeAsync("RegisterMissionConsole", missionId); + public Task UnregisterMissionConsole(Guid missionId) => Connection.InvokeAsync("UnregisterMissionConsole", missionId); + public Task RegisterMissionConsoles() => Connection.InvokeAsync("RegisterMissionConsoles"); + public Task UnregisterMissionConsoles() => Connection.InvokeAsync("UnregisterMissionConsoles"); + public Task RegisterAllConsoles() => Connection.InvokeAsync("RegisterAllConsoles"); + public Task UnregisterAllConsoles() => Connection.InvokeAsync("UnregisterAllConsoles"); + +} diff --git a/RobotNet.WebApp/Clients/DashboardHubClient.cs b/RobotNet.WebApp/Clients/DashboardHubClient.cs new file mode 100644 index 0000000..dc1771b --- /dev/null +++ b/RobotNet.WebApp/Clients/DashboardHubClient.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.Script.Shares.Dashboard; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class DashboardHubClient : WebAssemblyHubClient +{ + public event Action? DashboardDataUpdated; + private IDisposable? disDashboardDataUpdated; + + public DashboardHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + disDashboardDataUpdated = Connection.On("DashboardDataUpdated", data => DashboardDataUpdated?.Invoke(data)); + } + + public override async Task StopAsync() + { + if (disDashboardDataUpdated != null) + { + disDashboardDataUpdated.Dispose(); + disDashboardDataUpdated = null; + } + await base.StopAsync(); + } + + public async Task> GetDashboardData() + => IsConnected ? await Connection.InvokeAsync>(nameof(GetDashboardData)) : new(false, "Kết nối thất bại"); +} diff --git a/RobotNet.WebApp/Clients/HMIHubClient.cs b/RobotNet.WebApp/Clients/HMIHubClient.cs new file mode 100644 index 0000000..e1cbe8c --- /dev/null +++ b/RobotNet.WebApp/Clients/HMIHubClient.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.Script.Shares; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class HMIHubClient : WebAssemblyHubClient +{ + public event Action? StateChanged; + public event Action? RequestChanged; + + public HMIHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + Connection.On(nameof(StateChanged), OnStateChanged); + Connection.On(nameof(RequestChanged), OnRequestChanged); + + } + + + public async Task GetState() => await Connection.InvokeAsync(nameof(GetState)); + public async Task GetRequest() => await Connection.InvokeAsync(nameof(GetRequest)); + public async Task> GetVariables(IEnumerable keys) => await Connection.InvokeAsync>(nameof(GetVariables), keys); + public async Task SetVariable(string key, string value) => await Connection.InvokeAsync(nameof(SetVariable), key, value); + + private void OnStateChanged(ProcessorState state) => StateChanged?.Invoke(state); + private void OnRequestChanged(ProcessorRequest request) => RequestChanged?.Invoke(request); +} diff --git a/RobotNet.WebApp/Clients/ProcessorHubClient.cs b/RobotNet.WebApp/Clients/ProcessorHubClient.cs new file mode 100644 index 0000000..b93bb29 --- /dev/null +++ b/RobotNet.WebApp/Clients/ProcessorHubClient.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.Script.Shares; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class ProcessorHubClient : WebAssemblyHubClient +{ + public event Action? StateChanged; + public event Action? RequestChanged; + + public ProcessorHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + Connection.On(nameof(StateChanged), OnStateChanged); + Connection.On(nameof(RequestChanged), OnRequestChanged); + } + + public async Task GetState() => await Connection.InvokeAsync(nameof(GetState)); + public async Task GetRequest() => await Connection.InvokeAsync(nameof(GetRequest)); + public async Task Build() => await Connection.InvokeAsync(nameof(Build)); + public async Task Run() => await Connection.InvokeAsync(nameof(Run)); + public async Task Stop() => await Connection.InvokeAsync(nameof(Stop)); + public async Task Reset() => await Connection.InvokeAsync(nameof(Reset)); + + private void OnStateChanged(ProcessorState state) => StateChanged?.Invoke(state); + private void OnRequestChanged(ProcessorRequest request) => RequestChanged?.Invoke(request); + +} diff --git a/RobotNet.WebApp/Clients/RobotHubClient.cs b/RobotNet.WebApp/Clients/RobotHubClient.cs new file mode 100644 index 0000000..66a4781 --- /dev/null +++ b/RobotNet.WebApp/Clients/RobotHubClient.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.MapShares.Dtos; +using RobotNet.RobotShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class RobotHubClient: WebAssemblyHubClient +{ + public event Action>? IsOnlineChanged; + private IDisposable? disIsOnlineChanged; + + public event Action? MapDeactive; + private IDisposable? disMapDeactive; + + public event Action>? UpdateChanged; + private IDisposable? disUpdateChanged; + + public event Action>? ElementsStateChanged; + private IDisposable? disElementsStateChanged; + + public event Action? VDA5050InfoChanged; + private IDisposable? disVDA5050InfoChanged; + + public event Action? RobotDetailDeactive; + private IDisposable? disRobotDetailDeactive; + + public RobotHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + disIsOnlineChanged = Connection.On>("IsOnlineChanged", nodes => IsOnlineChanged?.Invoke(nodes)); + disUpdateChanged = Connection.On>("UpdateChanged", state => UpdateChanged?.Invoke(state)); + disElementsStateChanged = Connection.On>("ElementsStateChanged", state => ElementsStateChanged?.Invoke(state)); + disVDA5050InfoChanged = Connection.On("VDA5050InfoChanged", vdastate => VDA5050InfoChanged?.Invoke(vdastate)); + disMapDeactive = Connection.On("MapDeactive", () => MapDeactive?.Invoke()); + disRobotDetailDeactive = Connection.On("RobotDetailDeactive", () => RobotDetailDeactive?.Invoke()); + } + + public override async Task StopAsync() + { + if (disIsOnlineChanged != null) + { + disIsOnlineChanged.Dispose(); + disIsOnlineChanged = null; + } + if (disUpdateChanged != null) + { + disUpdateChanged.Dispose(); + disUpdateChanged = null; + } + if (disElementsStateChanged != null) + { + disElementsStateChanged.Dispose(); + disElementsStateChanged = null; + } + if (disVDA5050InfoChanged != null) + { + disVDA5050InfoChanged.Dispose(); + disVDA5050InfoChanged = null; + } + if (disMapDeactive != null) + { + disMapDeactive.Dispose(); + disMapDeactive = null; + } + if (disRobotDetailDeactive != null) + { + disRobotDetailDeactive.Dispose(); + disRobotDetailDeactive = null; + } + await base.StopAsync(); + } + + public Task MapActive(Guid mapId) => IsConnected ? Connection.InvokeAsync("MapActive", mapId) : Task.CompletedTask; + + public Task RobotDetailActive(string robotId) => IsConnected ? Connection.InvokeAsync("RobotDetailActive", robotId) : Task.CompletedTask; + + public async Task SetInitialPose(string robotId, double x, double y, double yaw) + => IsConnected ? await Connection.InvokeAsync(nameof(SetInitialPose), robotId, x, y, yaw) : new(false, "Kết nối thất bại"); + + public async Task MoveStraight(string robotId, double x, double y, double theta) + => IsConnected ? await Connection.InvokeAsync(nameof(MoveStraight), robotId, x, y, theta) : new(false, "Kết nối thất bại"); + + public async Task MoveToNode(string robotId, string nodename) + => IsConnected ? await Connection.InvokeAsync(nameof(MoveToNode), robotId, nodename) : new(false, "Kết nối thất bại"); + + public async Task MoveRandom(string robotId, List nodes) + => IsConnected ? await Connection.InvokeAsync(nameof(MoveRandom), robotId, nodes) : new(false, "Kết nối thất bại"); + + public async Task CancelNavigation(string robotId) + => IsConnected ? await Connection.InvokeAsync(nameof(CancelNavigation), robotId) : new(false, "Kết nối thất bại"); + + public async Task SetMap(string robotId, Guid mapId) + => IsConnected ? await Connection.InvokeAsync(nameof(SetMap), robotId, mapId) : new(false, "Kết nối thất bại"); + + public async Task CancelAction(string robotId) + => IsConnected ? await Connection.InvokeAsync(nameof(CancelAction), robotId) : new(false, "Kết nối thất bại"); + + public async Task SendAction(string robotId, ActionDto action) + => IsConnected ? await Connection.InvokeAsync(nameof(SendAction), robotId, action) : new(false, "Kết nối thất bại"); + +} diff --git a/RobotNet.WebApp/Clients/ScriptTaskHubClient.cs b/RobotNet.WebApp/Clients/ScriptTaskHubClient.cs new file mode 100644 index 0000000..744a33c --- /dev/null +++ b/RobotNet.WebApp/Clients/ScriptTaskHubClient.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class ScriptTaskHubClient : WebAssemblyHubClient +{ + public event Action? TaskPaused; + public event Action? TaskResumed; + + public ScriptTaskHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + Connection.On(nameof(TaskPaused), OnTaskPaused); + Connection.On(nameof(TaskResumed), OnTaskResumed); + } + + public async Task Pause(string name) => await Connection.InvokeAsync(nameof(Pause), name); + public async Task Resume(string name) => await Connection.InvokeAsync(nameof(Resume), name); + public async Task> GetTaskStates() => await Connection.InvokeAsync>(nameof(GetTaskStates)); + + + private void OnTaskPaused(string name) => TaskPaused?.Invoke(name); + private void OnTaskResumed(string name) => TaskResumed?.Invoke(name); +} diff --git a/RobotNet.WebApp/Clients/TrafficHubClient.cs b/RobotNet.WebApp/Clients/TrafficHubClient.cs new file mode 100644 index 0000000..e415f61 --- /dev/null +++ b/RobotNet.WebApp/Clients/TrafficHubClient.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.SignalR.Client; +using RobotNet.RobotShares.Dtos; +using RobotNet.Shares; + +namespace RobotNet.WebApp.Clients; + +public class TrafficHubClient: WebAssemblyHubClient +{ + public event Action>? TrafficAgentUpdated; + private IDisposable? disTrafficAgentUpdated; + + public event Action? MapDeactive; + private IDisposable? disMapDeactive; + + public TrafficHubClient(IAccessTokenProvider tokenProvider, Uri uri) : base(tokenProvider, uri) + { + disTrafficAgentUpdated = Connection.On>("TrafficAgentUpdated", agents => TrafficAgentUpdated?.Invoke(agents)); + disMapDeactive = Connection.On("TrafficManagerDeactive", () => MapDeactive?.Invoke()); + + } + + public override async Task StopAsync() + { + if (disTrafficAgentUpdated != null) + { + disTrafficAgentUpdated.Dispose(); + disTrafficAgentUpdated = null; + } + if (disMapDeactive != null) + { + disMapDeactive.Dispose(); + disMapDeactive = null; + } + await base.StopAsync(); + } + + public Task TrafficActive(Guid mapId) => IsConnected ? Connection.InvokeAsync("TrafficActive", mapId) : Task.CompletedTask; + + public async Task>> LoadTrafficMaps() + => IsConnected ? await Connection.InvokeAsync>>(nameof(LoadTrafficMaps)) : new(false, "Kết nối thất bại"); + + public async Task> LoadTrafficMap(Guid mapId) + => IsConnected ? await Connection.InvokeAsync>(nameof(LoadTrafficMap), mapId) : new(false, "Kết nối thất bại"); +} diff --git a/RobotNet.WebApp/Clients/WebAssemblyHubClient.cs b/RobotNet.WebApp/Clients/WebAssemblyHubClient.cs new file mode 100644 index 0000000..dc6157d --- /dev/null +++ b/RobotNet.WebApp/Clients/WebAssemblyHubClient.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using RobotNet.Clients; + +namespace RobotNet.WebApp.Clients; + +public abstract class WebAssemblyHubClient(IAccessTokenProvider tokenProvider, Uri uri) : HubClient(uri, async () => + { + var tokenResult = await tokenProvider.RequestAccessToken(); + return tokenResult.TryGetToken(out var token) ? token.Value : ""; + }) +{ +} diff --git a/RobotNet.WebApp/Components/ConfirmDialog.razor b/RobotNet.WebApp/Components/ConfirmDialog.razor new file mode 100644 index 0000000..f527df7 --- /dev/null +++ b/RobotNet.WebApp/Components/ConfirmDialog.razor @@ -0,0 +1,33 @@ + + + @Content + + + @CancelText + @ConfirmText + + + +@code { + [CascadingParameter] + private IMudDialogInstance? MudDialog { get; set; } + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public string? Content { get; set; } + + [Parameter] + public string? ConfirmText { get; set; } + + [Parameter] + public string? CancelText { get; set; } = "Cancel"; + + [Parameter] + public Color Color { get; set; } + + private void Submit() => MudDialog?.Close(DialogResult.Ok(true)); + + private void Cancel() => MudDialog?.Cancel(); +} diff --git a/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor b/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor new file mode 100644 index 0000000..2028fa9 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor @@ -0,0 +1,29 @@ +
+ @Title + @Data + @Unit +
+ +@code { + [Parameter] + public string Title { get; set; } = string.Empty; + + [Parameter] + public string Unit { get; set; } = string.Empty; + + [Parameter] + public string Color { get; set; } = "var(--dashboard-text-common-color)"; + + [Parameter] + public string Data { get; set; } = string.Empty; + + [Parameter] + public string Class { get; set; } = string.Empty; + + [Parameter] + public string Style { get; set; } = string.Empty; + + [Parameter] + public string Width { get; set; } = "14.17%"; + +} diff --git a/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor.css b/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor.css new file mode 100644 index 0000000..fc6970e --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/DailyComponentData.razor.css @@ -0,0 +1,47 @@ +.paper { + background: var(--dashboard-card-background-color); + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease, box-shadow 0.3s ease; + border-radius: 16px; + position: relative; + overflow: hidden; +} + + .paper:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); + } + + .paper::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--dashboard-card-background-color); + transition: left 0.5s ease; + } + + .paper .text-title { + font-size: 1.1vw; + align-items: center; + color: var(--dashboard-text-white-color); + font-family: var(--dashboard-text-font-family); + } + + .paper .text-data { + font-size: 2.5vw; + font-weight: bold; + color: var(--dashboard-text-white-color); + } + + .paper .text-unit { + font-size: 0.8vw; + color: var(--dashboard-text-white-color); + font-family: var(--dashboard-text-font-family); + } \ No newline at end of file diff --git a/RobotNet.WebApp/Dashboard/Components/DailyData.razor b/RobotNet.WebApp/Dashboard/Components/DailyData.razor new file mode 100644 index 0000000..fb2a806 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/DailyData.razor @@ -0,0 +1,29 @@ +@using RobotNet.Script.Shares.Dashboard +
+ + + + + + + + +
+ +@code { + private string width = "12.25%"; + + private DailyMissionDto DataParam = new(); + public void UpdateData(DailyMissionDto data) + { + DataParam.Completed = data.Completed; + DataParam.Error = data.Error; + DataParam.Total = data.Total; + DataParam.CompletedRate = data.CompletedRate; + DataParam.TaktTimeMin = data.TaktTimeMin; + DataParam.TaktTimeAverage = data.TaktTimeAverage; + DataParam.TaktTimeMax = data.TaktTimeMax; + DataParam.RobotOnline = data.RobotOnline; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor b/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor new file mode 100644 index 0000000..1e10884 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor @@ -0,0 +1,134 @@ +@using RobotNet.Script.Shares.Dashboard +@using RobotNet.WebApp.Charts.Components +@using RobotNet.WebApp.Charts.Core +@using RobotNet.WebApp.Charts.Models.BarChart +@using RobotNet.WebApp.Charts.Models.BarChart.Axes +@using RobotNet.WebApp.Charts.Models.Common.Dataset +@using RobotNet.WebApp.Charts.Enums + +
+ +
+ +@code { + private BarChart ChartRef = default!; + private BarChartOptions ChartOptions = default!; + private ChartData ChartData = default!; + + private DailyPerformanceDto[] DataParam = []; + + protected override void OnInitialized() + { + var completed = new BarChartDataset() { Label = "Completed", BackgroundColor = [ChartColors.GreenStr], Data = [.. DataParam.Select(d => d.Completed)] }; + var error = new BarChartDataset() { Label = "Error", BackgroundColor = [ChartColors.OrangeStr], Data = [.. DataParam.Select(d => d.Error)] }; + var other = new BarChartDataset() { Label = "Other", BackgroundColor = [ChartColors.GrayStr], Data = [.. DataParam.Select(d => d.Other)] }; + ChartData = new ChartData { Labels = [.. DataParam.Select(d => d.Label)], Datasets = [completed, error, other] }; + + ChartOptions = new() + { + Responsive = true, + Scales = new() + { + X = new() + { + Title = new ChartAxesTitle + { + Text = "Day", + Display = true, + Color = "#d6d6db", + Font = new() + { + Family = "Roboto", + Size = 15, + } + }, + Stacked = true, + Grid = new() + { + Display = false, + }, + Ticks = new() + { + Color = "#d6d6db" + } + }, + Y = new() + { + Title = new ChartAxesTitle + { + Text = "Mission", + Display = true, + Color = "#d6d6db", + Font = new() + { + Family = "Roboto", + Size = 15, + }, + }, + Min = 0, + Stacked = true, + Grid = new() + { + Display = false, + }, + Ticks = new() + { + Color = "#d6d6db" + } + }, + }, + Plugins = new() + { + Datalabels = new() + { + Color = "#d6d6db", + BorderColor = "transparent", + }, + Title = new() + { + Color = "#d6d6db", + Text = "Weekly AMR Performance", + Font = new() + { + Family = "Roboto", + Size = 20, + Weight = "normal", + }, + }, + Legend = new() + { + Display = true, + Labels = new() + { + Color = "#d6d6db", + Font = new() + { + Family = "Roboto", + Weight = "normal", + Size = 15, + } + } + } + }, + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await ChartRef.InitializeAsync(chartData: ChartData, chartOptions: ChartOptions); + } + await base.OnAfterRenderAsync(firstRender); + } + + public async Task UpdateData(DailyPerformanceDto[] dataParam) + { + var completed = new BarChartDataset() { Label = "Completed", BackgroundColor = [ChartColors.GreenStr], Data = [.. dataParam.Select(d => d.Completed)] }; + var error = new BarChartDataset() { Label = "Error", BackgroundColor = [ChartColors.OrangeStr], Data = [.. dataParam.Select(d => d.Error)] }; + var other = new BarChartDataset() { Label = "Other", BackgroundColor = [ChartColors.GrayStr], Data = [.. dataParam.Select(d => d.Other)] }; + ChartData = new ChartData { Labels = [.. dataParam.Select(d => d.Label)], Datasets = [completed, error, other] }; + + await ChartRef.UpdateValuesAsync(ChartData); + } +} diff --git a/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor.css b/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor.css new file mode 100644 index 0000000..fc21cb4 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/MissionsPerformanceBarChart.razor.css @@ -0,0 +1,30 @@ +.paper { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + position: relative; + padding: 0 1rem 0 1rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease, box-shadow 0.3s ease; + background: var(--dashboard-card-background-color); + border-radius: 16px; + overflow: hidden; +} + + .paper:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); + } + + .paper::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--dashboard-card-background-color); + transition: left 0.5s ease; + } diff --git a/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor b/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor new file mode 100644 index 0000000..8c0e782 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor @@ -0,0 +1,90 @@ +@using RobotNet.Script.Shares.Dashboard +@using RobotNet.WebApp.Charts.Components +@using RobotNet.WebApp.Charts.Core +@using RobotNet.WebApp.Charts.Models.Common.Dataset +@using RobotNet.WebApp.Charts.Models.PieChart +@using RobotNet.WebApp.Charts.Enums + +
+ +
@($"{CompletedPercent}%")
+
+ +@code { + [Parameter] + public string ChartName { get; set; } = ""; + + private PieChart ChartRef = default!; + private PieChartOptions ChartOptions = default!; + private ChartData ChartData = default!; + private PieChartDataset ChartDataSet = default!; + + private int CompletedPercent = 0; + + protected override void OnInitialized() + { + base.OnInitialized(); + + ChartDataSet = new PieChartDataset() { BackgroundColor = [ChartColors.GreenStr, ChartColors.OrangeStr, ChartColors.GrayStr], Data = [90, 10, 10] }; + ChartData = new ChartData { Labels = ["Completed", "Error", "Other"], Datasets = [ChartDataSet] }; + + ChartOptions = new() + { + Responsive = true, + Cutout = "50%", + Plugins = new() + { + Datalabels = new() + { + Color = "#d6d6db", + BorderColor = "transparent", + }, + Title = new() + { + Color = "#d6d6db", + Text = ChartName, + Font = new() + { + Family = "Roboto", + Size = 20, + Weight = "normal", + }, + }, + Legend = new() + { + Display = false, + Position = LegendPosition.Top, + Labels = new() + { + Color = "#d6d6db", + Font = new() + { + Family = "Roboto", + Weight = "normal", + Size = 15, + } + }, + } + }, + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + await ChartRef.InitializeAsync(chartData: ChartData, chartOptions: ChartOptions); + } + + public async Task UpdateData(DailyPerformanceDto data) + { + ChartDataSet = new PieChartDataset() { BackgroundColor = [ChartColors.GreenStr, ChartColors.OrangeStr, ChartColors.GrayStr], Data = [data.Completed, data.Error, data.Other] }; + ChartData = new ChartData { Labels = ["Completed", "Error", "Other"], Datasets = [ChartDataSet] }; + await ChartRef.UpdateValuesAsync(ChartData); + + if (data.Completed + data.Error + data.Other != 0) CompletedPercent = (int)(data.Completed * 100.0 / (data.Completed + data.Error + data.Other) + 0.5); + else CompletedPercent = 0; + + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor.css b/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor.css new file mode 100644 index 0000000..4123ffa --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/PerformancePieChart.razor.css @@ -0,0 +1,44 @@ +.paper { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + position: relative; + padding: 0 1rem 1rem 1rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + transition: transform 0.3s ease, box-shadow 0.3s ease; + background: var(--dashboard-card-background-color); + border-radius: 16px; + overflow: hidden; +} + + .paper:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + } + + .paper::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--dashboard-card-background-color); + transition: left 0.5s ease; + } + + .paper .text-percent { + font-size: 3vw; + line-height: 3vw; + color: #00cc00; + font-weight: bold; + position: absolute; + top: 48%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + pointer-events: none; + padding: 50px 0 12px 0; + } diff --git a/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor b/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor new file mode 100644 index 0000000..f87db32 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor @@ -0,0 +1,122 @@ +@using RobotNet.Script.Shares.Dashboard +@using RobotNet.WebApp.Charts.Components +@using RobotNet.WebApp.Charts.Core +@using RobotNet.WebApp.Charts.Models.Common +@using RobotNet.WebApp.Charts.Models.Common.Dataset +@using RobotNet.WebApp.Charts.Models.LineChart +@using RobotNet.WebApp.Charts.Enums + +
+ +
+ +@code { + private LineChart ChartRef = default!; + private LineChartOptions ChartOptions = default!; + private ChartData ChartData = default!; + + private TaktTimeMissionDto[] DataParam = []; + + protected override void OnInitialized() + { + var minDataset = new LineChartDataset() { Label = "Min", BackgroundColor = [ChartColors.ColorList[0]], BorderColor = [ChartColors.ColorList[0]], Data = [..DataParam.Select(d => d.Min)]}; + var AverageDataset = new LineChartDataset() { Label = "Average", BackgroundColor = [ChartColors.ColorList[1]], BorderColor = [ChartColors.ColorList[1]], Data = [.. DataParam.Select(d => d.Average)] }; + var maxDataset = new LineChartDataset() { Label = "Max", BackgroundColor = [ChartColors.ColorList[2]], BorderColor = [ChartColors.ColorList[2]], Data = [.. DataParam.Select(d => d.Max)] }; + ChartData = new ChartData { Labels = [.. DataParam.Select(d => d.Label)], Datasets = [minDataset, AverageDataset, maxDataset] }; + + ChartOptions = new() + { + Responsive = true, + Interaction = new() { Mode = InteractionMode.Index, Intersect = false }, + Scales = new() + { + X = new() + { + Title = new() + { + Text = "Day", + Display = true, + Color = "#d6d6db", + }, + Grid = new() + { + Display = false, + }, + Ticks = new() + { + Color = "#d6d6db" + } + }, + Y = new() + { + Title = new() + { + Text = "Minute", + Display = true, + Color = "#d6d6db", + }, + Grid = new() + { + Display = false, + }, + Ticks = new() + { + Color = "#d6d6db" + } + }, + }, + Plugins = new() + { + Datalabels = new() + { + Display = false, + }, + Legend = new() + { + Display = true, + Labels = new() + { + Color = "#d6d6db", + Font = new() + { + Family = "Roboto", + Weight = "normal", + Size = 15, + }, + } + }, + Title = new() + { + FullSize = false, + Color = "#d6d6db", + Text = "AMR Takt Time", + Font = new() + { + Family = "Roboto", + Size = 20, + Weight = "normal", + }, + }, + }, + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await ChartRef.InitializeAsync(chartData: ChartData, chartOptions: ChartOptions); + } + await base.OnAfterRenderAsync(firstRender); + } + + public async Task UpdateData(TaktTimeMissionDto[] dataParam) + { + var minDataset = new LineChartDataset() { Label = "Min", BackgroundColor = [ChartColors.ColorList[0]], BorderColor = [ChartColors.ColorList[0]], Data = [.. dataParam.Select(d => d.Min)] }; + var AverageDataset = new LineChartDataset() { Label = "Average", BackgroundColor = [ChartColors.ColorList[1]], BorderColor = [ChartColors.ColorList[1]], Data = [.. dataParam.Select(d => d.Average)] }; + var maxDataset = new LineChartDataset() { Label = "Max", BackgroundColor = [ChartColors.ColorList[2]], BorderColor = [ChartColors.ColorList[2]], Data = [.. dataParam.Select(d => d.Max)] }; + ChartData = new ChartData { Labels = [.. dataParam.Select(d => d.Label)], Datasets = [minDataset, AverageDataset, maxDataset] }; + + await ChartRef.UpdateValuesAsync(ChartData); + } +} diff --git a/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor.css b/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor.css new file mode 100644 index 0000000..fc21cb4 --- /dev/null +++ b/RobotNet.WebApp/Dashboard/Components/TaktTimeLineChart.razor.css @@ -0,0 +1,30 @@ +.paper { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + position: relative; + padding: 0 1rem 0 1rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + transition: transform 0.3s ease, box-shadow 0.3s ease; + background: var(--dashboard-card-background-color); + border-radius: 16px; + overflow: hidden; +} + + .paper:hover { + transform: translateY(-2px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); + } + + .paper::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: var(--dashboard-card-background-color); + transition: left 0.5s ease; + } diff --git a/RobotNet.WebApp/Dockerfile b/RobotNet.WebApp/Dockerfile new file mode 100644 index 0000000..e9cf465 --- /dev/null +++ b/RobotNet.WebApp/Dockerfile @@ -0,0 +1,61 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY ["RobotNet.WebApp/RobotNet.WebApp.csproj", "RobotNet.WebApp/"] +COPY ["RobotNet.WebApp/libman.json", "RobotNet.WebApp/"] +COPY ["RobotNet.MapShares/RobotNet.MapShares.csproj", "RobotNet.MapShares/"] +COPY ["RobotNet.RobotShares/RobotNet.RobotShares.csproj", "RobotNet.RobotShares/"] +COPY ["RobotNet.Script.Shares/RobotNet.Script.Shares.csproj", "RobotNet.Script.Shares/"] +COPY ["RobotNet.Script/RobotNet.Script.csproj", "RobotNet.Script/"] +COPY ["RobotNet.Script.Expressions/RobotNet.Script.Expressions.csproj", "RobotNet.Script.Expressions/"] +COPY ["RobotNet.Shares/RobotNet.Shares.csproj", "RobotNet.Shares/"] +COPY ["RobotNet.Clients/RobotNet.Clients.csproj", "RobotNet.Clients/"] + +RUN dotnet restore "RobotNet.WebApp/RobotNet.WebApp.csproj" + +WORKDIR /src/RobotNet.WebApp +RUN dotnet tool install -g Microsoft.Web.LibraryManager.Cli +ENV PATH="${PATH}:/root/.dotnet/tools" +RUN libman restore + +WORKDIR /src +COPY RobotNet.WebApp/ RobotNet.WebApp/ +COPY RobotNet.MapShares/ RobotNet.MapShares/ +COPY RobotNet.RobotShares/ RobotNet.RobotShares/ +COPY RobotNet.Script.Shares/ RobotNet.Script.Shares/ +COPY RobotNet.Script/ RobotNet.Script/ +COPY RobotNet.Script.Expressions/ RobotNet.Script.Expressions/ +COPY RobotNet.Shares/ RobotNet.Shares/ +COPY RobotNet.Clients/ RobotNet.Clients/ + +RUN rm -rf ./RobotNet.WebApp/bin +RUN rm -rf ./RobotNet.WebApp/obj +RUN rm -rf ./RobotNet.MapShares/bin +RUN rm -rf ./RobotNet.MapShares/obj +RUN rm -rf ./RobotNet.RobotShares/bin +RUN rm -rf ./RobotNet.RobotShares/obj +RUN rm -rf ./RobotNet.cript.Shares/bin +RUN rm -rf ./RobotNet.cript.Shares/obj +RUN rm -rf ./RobotNet.Script/bin +RUN rm -rf ./RobotNet.Script/obj +RUN rm -rf ./RobotNet.Script.Expressions/bin +RUN rm -rf ./RobotNet.Script.Expressions/obj +RUN rm -rf ./RobotNet.Shares/bin +RUN rm -rf ./RobotNet.Shares/obj +RUN rm -rf ./RobotNet.Clients/bin +RUN rm -rf ./RobotNet.Clients/obj + +WORKDIR "/src/RobotNet.WebApp" +RUN dotnet build "RobotNet.WebApp.csproj" -c Release -o /app/build + +FROM build AS publish +WORKDIR /src/RobotNet.WebApp +RUN dotnet publish "RobotNet.WebApp.csproj" -c Release -o /app/publish --no-restore + +FROM nginx:alpine AS final +WORKDIR /usr/share/nginx/html + +COPY --from=publish /app/publish/wwwroot . +COPY RobotNet.WebApp/nginx.conf /etc/nginx/nginx.conf + +ENTRYPOINT ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/RobotNet.WebApp/Helpers/DefaultPersistentStorageConfigurationExtensions.cs b/RobotNet.WebApp/Helpers/DefaultPersistentStorageConfigurationExtensions.cs new file mode 100644 index 0000000..b9f2810 --- /dev/null +++ b/RobotNet.WebApp/Helpers/DefaultPersistentStorageConfigurationExtensions.cs @@ -0,0 +1,23 @@ +using System.Reflection; + +namespace RobotNet.WebApp.Helpers; + +public static class DefaultPersistentStorageConfigurationExtensions +{ + /// + /// Sửa lỗi Exception "System.Diagnostics.Process is not supported on this platform." + /// trong static constructor của Microsoft.CodeAnalysis.Host.DefaultPersistentStorageConfiguration + /// khi gọi CompletionService.GetCompletionsAsync + /// + public static void FixStaticConstructorException() + { + try + { + _ = typeof(Microsoft.CodeAnalysis.Host.HostServices).Assembly.CreateInstance("Microsoft.CodeAnalysis.Host.DefaultPersistentStorageConfiguration"); + } + catch (TargetInvocationException) + { + GC.Collect(); + } + } +} diff --git a/RobotNet.WebApp/Helpers/ServiceCollectionExtensions.cs b/RobotNet.WebApp/Helpers/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..ee5b268 --- /dev/null +++ b/RobotNet.WebApp/Helpers/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using System.Net.Http.Headers; + +namespace RobotNet.WebApp.Helpers; + +public static class ServiceCollectionExtensions +{ + public static IHttpClientBuilder AddAuthorizationHttpClient(this IServiceCollection services, string name, string baseAddress) + { + return services.AddHttpClient(name, client => + { + client.BaseAddress = new Uri(baseAddress); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + }) + .AddHttpMessageHandler(sp => + { + var tokenProvider = sp.GetRequiredService(); + var navigation = sp.GetRequiredService(); + var messageHandle = new AuthorizationMessageHandler(tokenProvider, navigation); + + messageHandle.ConfigureHandler([baseAddress]); + return messageHandle; + }) + .RemoveAllLoggers(); + } +} diff --git a/RobotNet.WebApp/Layout/HMILayout.razor b/RobotNet.WebApp/Layout/HMILayout.razor new file mode 100644 index 0000000..c7b673d --- /dev/null +++ b/RobotNet.WebApp/Layout/HMILayout.razor @@ -0,0 +1,12 @@ +@inherits LayoutComponentBase + +
+
+ @Body +
+
+ + + + + diff --git a/RobotNet.WebApp/Layout/MainLayout.razor b/RobotNet.WebApp/Layout/MainLayout.razor new file mode 100644 index 0000000..759b36d --- /dev/null +++ b/RobotNet.WebApp/Layout/MainLayout.razor @@ -0,0 +1,13 @@ +@inherits LayoutComponentBase + + +
+
+ @Body +
+
+ + + + + diff --git a/RobotNet.WebApp/Layout/NavMenu.razor b/RobotNet.WebApp/Layout/NavMenu.razor new file mode 100644 index 0000000..2c61653 --- /dev/null +++ b/RobotNet.WebApp/Layout/NavMenu.razor @@ -0,0 +1,67 @@ + + + + +@code { + public class NavModel + { + public string Icon { get; set; } = ""; + public string Path { get; set; } = ""; + public string Label { get; set; } = ""; + public NavLinkMatch Match { get; set; } + } + + public NavModel[] Navs = [ + new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All}, + new(){Icon = "mdi-file-cog", Path="/script-manager", Label = "Script Manager", Match = NavLinkMatch.All}, + new(){Icon = "mdi-file-code", Path="/script-editor", Label = "Script Editor", Match = NavLinkMatch.All}, + new(){Icon = "mdi-flag-checkered", Path="/missions", Label = "Missions", Match = NavLinkMatch.All}, + new(){Icon = "mdi-map-legend", Path="/navigation-maps", Label = "Maps", Match = NavLinkMatch.All}, + new(){Icon = "mdi-robot-mower", Path="/robots", Label = "Robots", Match = NavLinkMatch.All}, + new(){Icon = "mdi-robot-industrial", Path="/robots/model", Label = "Robot Models", Match = NavLinkMatch.All}, + new(){Icon = "mdi-monitor-eye", Path="/robots/monitor", Label = "Monitor", Match = NavLinkMatch.All}, + new(){Icon = "mdi-traffic-light", Path="/traffic-manager", Label = "Traffic", Match = NavLinkMatch.All}, + new(){Icon = "mdi-factory", Path="/open-acs", Label = "Open ACS", Match = NavLinkMatch.All}, + new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All}, + new(){Icon = "mdi-account", Path="/user", Label = "User", Match = NavLinkMatch.All}, + ]; + + private bool collapseNavMenu = true; + + private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + + private void ToggleNavMenu() + { + collapseNavMenu = !collapseNavMenu; + } +} diff --git a/RobotNet.WebApp/Layout/NavMenu.razor.css b/RobotNet.WebApp/Layout/NavMenu.razor.css new file mode 100644 index 0000000..79f7058 --- /dev/null +++ b/RobotNet.WebApp/Layout/NavMenu.razor.css @@ -0,0 +1,117 @@ +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); + height: 100%; + width: 250px; + display: flex; + flex-direction: column; + transition: width 0.3s; +} + + .sidebar.hidden { + display: none; + } + + .sidebar .title { + margin: 8px 4px 0 4px; + flex-direction: row; + justify-content: center; + align-items: center; + display: flex; + } + + .sidebar .title button { + height: 35px; + width: 35px; + justify-content: center; + align-items: center; + overflow: hidden; + } + + .sidebar:not(.collapsed) .title button { + display: none; + } + + .sidebar.collapsed { + width: 74px; + } + + .sidebar.collapsed .title { + justify-content: center; + } + + .sidebar.collapsed .title img { + display: none; + } + + .sidebar.collapsed .title button { + display: flex; + } + + /*.sidebar .title .button { + display: flex; + border-radius: 20px; + }*/ + + .sidebar .title .button:hover { + background-color: rgb(5, 39, 80); + } + + .sidebar .user { + display: flex; + flex-direction: row; + align-items: center; + } + + .sidebar.collapsed .user > div { + display: none !important; + } + + .sidebar.collapsed .nav-label { + display: none; + } + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; + overflow: hidden; +} + + .nav-item .nav-label { + font-size: 18px; + text-wrap: nowrap; + } + + .nav-item .nav-icon { + height: 48px; + width: 48px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + } + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep a { + color: #d7d7d7; + border-radius: 4px; + height: 48px; + display: flex; + align-items: center; + } + + .nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; + } + + .nav-item ::deep a:hover { + background-color: rgba(255,255,255,0.1); + color: white; + } diff --git a/RobotNet.WebApp/Layout/RedirectToLogin.razor b/RobotNet.WebApp/Layout/RedirectToLogin.razor new file mode 100644 index 0000000..a1cf400 --- /dev/null +++ b/RobotNet.WebApp/Layout/RedirectToLogin.razor @@ -0,0 +1,9 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject NavigationManager Navigation + +@code { + protected override void OnInitialized() + { + Navigation.NavigateToLogin("authentication/login"); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor new file mode 100644 index 0000000..b8fef4a --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor @@ -0,0 +1,121 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + + +@code { + [Parameter, EditorRequired] + public EdgeModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + private ElementReference Ref; + private ElementReference ErrorRef; + private DotNetObjectReference DotNetObj = null!; + private bool IsError = false; + + private bool IsSetting => EditorState == EditorState.NavigationEdit || EditorState == EditorState.CreateStraighEdge || EditorState == EditorState.CreateCurveEdge || + EditorState == EditorState.CreateDoubleCurveEdge; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("AddEventListener", DotNetObj, Ref, "click", nameof(Click), false); + await UpdatePathData(); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", ErrorRef, "visibility", "hidden"); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateLine = false; + if (parameters.TryGetValue(nameof(Model), out EdgeModel? model)) + { + if (((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) + { + Model.StartNodePositionChanged -= UpdatePathData; + Model.EndNodePositionChanged -= UpdatePathData; + Model.ControlPoint1PositionChanged -= UpdatePathData; + Model.ControlPoint2PositionChanged -= UpdatePathData; + Model.ActiveChanged -= ActivedChanged; + Model.ErrorChanged -= ErrorChanged; + } + + if (model != null) + { + model.StartNodePositionChanged += UpdatePathData; + model.EndNodePositionChanged += UpdatePathData; + model.ControlPoint1PositionChanged += UpdatePathData; + model.ControlPoint2PositionChanged += UpdatePathData; + model.ActiveChanged += ActivedChanged; + model.ErrorChanged += ErrorChanged; + + updateLine = true; + } + } + } + + await base.SetParametersAsync(parameters); + if (updateLine) await UpdatePathData(); + } + + private async Task UpdatePathData() + { + var data = $"M {Model.X1} {Model.Y1}"; + + if (Model.TrajectoryDegree == TrajectoryDegree.One) + { + data = $"{data} L {(Model.X1 + Model.X2) / 2} {(Model.Y1 + Model.Y2) / 2} L {Model.X2} {Model.Y2}"; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Two) + { + data = $"{data} Q {Model.ControlPoint1X} {Model.ControlPoint1Y} {Model.X2} {Model.Y2}"; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Three) + { + data = $"{data} C {Model.ControlPoint1X} {Model.ControlPoint1Y}, {Model.ControlPoint2X} {Model.ControlPoint2Y}, {Model.X2} {Model.Y2}"; + } + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "d", data); + if (IsError) await JSRuntime.InvokeVoidAsync("ElementSetAttribute", ErrorRef, "d", data); + } + + private async void ActivedChanged(bool state) => await JSRuntime.InvokeVoidAsync(state ? "AddSelected" : "RemoveSelected", Ref); + + private async void ErrorChanged(bool state) + { + IsError = state; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", ErrorRef, "visibility", state ? "visible" : "hidden"); + await UpdatePathData(); + } + + [JSInvokable] + public void Click() => Model.Selected(); + + public void Dispose() + { + if (Model != null) + { + Model.StartNodePositionChanged -= UpdatePathData; + Model.EndNodePositionChanged -= UpdatePathData; + Model.ControlPoint1PositionChanged -= UpdatePathData; + Model.ControlPoint2PositionChanged -= UpdatePathData; + Model.ActiveChanged -= ActivedChanged; + Model.ErrorChanged -= ErrorChanged; + } + if (DotNetObj != null) DotNetObj.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor.css new file mode 100644 index 0000000..6ac0298 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/Edge.razor.css @@ -0,0 +1,24 @@ +path { + cursor: default; + stroke: #22B3FF; + stroke-width: var(--edge-stroke-width); + fill: none; +} + + path.setting:hover { + stroke: #1E9DDF; + cursor: pointer; + } + + path.setting:active { + stroke: #227CFF; + } + + path.setting.active:hover { + stroke: #227CFF; + } + + path.active { + stroke: #227CFF; + } + diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor new file mode 100644 index 0000000..aa4dfa4 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor @@ -0,0 +1,128 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + + + + + + + + + + + + +@code { + private ElementReference CP1Ref; + private ElementReference CP2Ref; + private DotNetObjectReference DotNetObj = null!; + + private double X1; + private double Y1; + private double CX1; + private double CY1; + private double CX2; + private double CY2; + private double X2; + private double Y2; + private string EdgeData = ""; + private EdgeModel? Model; + private string Visibility = "hidden"; + + private string ControlPoint2Visibility = "hidden"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, CP1Ref, nameof(OnMouseDown1), false); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, CP2Ref, nameof(OnMouseDown2), false); + } + + public void SetControl(EdgeModel? model) + { + if (Model != null) + { + Model.StartNodePositionChanged -= CaculatePath; + Model.EndNodePositionChanged -= CaculatePath; + Model.ControlPoint1PositionChanged -= CaculatePath; + Model.ControlPoint2PositionChanged -= CaculatePath; + } + + Model = model; + if (Model != null) + { + Visibility = "visible"; + if (Model.TrajectoryDegree == TrajectoryDegree.Three) ControlPoint2Visibility = "visible"; + CaculatePath(); + + Model.StartNodePositionChanged += CaculatePath; + Model.EndNodePositionChanged += CaculatePath; + Model.ControlPoint1PositionChanged += CaculatePath; + Model.ControlPoint2PositionChanged += CaculatePath; + if (Model.TrajectoryDegree == TrajectoryDegree.Three) ControlPoint2Visibility = "visible"; + else ControlPoint2Visibility = "hidden"; + Visibility = "visible"; + } + else + { + Visibility = "hidden"; + ControlPoint2Visibility = "hidden"; + } + StateHasChanged(); + } + + private Task CaculatePath() + { + if (Model == null) return Task.CompletedTask; + + X1 = Model.X1; + Y1 = Model.Y1; + X2 = Model.X2; + Y2 = Model.Y2; + CX1 = Model.ControlPoint1X; + CY1 = Model.ControlPoint1Y; + CX2 = Model.ControlPoint2X; + CY2 = Model.ControlPoint2Y; + StateHasChanged(); + return Task.CompletedTask; + } + + [JSInvokable] + public void OnMouseDown1(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActivedControlPoint1 = true; + Model.ActivedControlPoint2 = false; + } + } + + [JSInvokable] + public void OnMouseDown2(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActivedControlPoint2 = true; + Model.ActivedControlPoint1 = false; + } + } + + public void Dispose() + { + if (Model != null) + { + Model.StartNodePositionChanged -= CaculatePath; + Model.EndNodePositionChanged -= CaculatePath; + Model.ControlPoint1PositionChanged -= CaculatePath; + Model.ControlPoint2PositionChanged -= CaculatePath; + Model = null; + } + + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor.css new file mode 100644 index 0000000..2daf0f2 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeControlPoint.razor.css @@ -0,0 +1,23 @@ +circle { + cursor: default; + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} + + circle:hover { + stroke-width: 0.02px; + } + + circle:active { + stroke-width: 0.02px; + cursor: pointer; + stroke: green; + } + +line { + stroke-width: 0.03px; + stroke: #d63384; + stroke-dasharray: 0.1 0.1; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor new file mode 100644 index 0000000..c7aad69 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor @@ -0,0 +1,163 @@ +@inject IJSRuntime JSRuntime + + + + + + + + + + + + + + + +@code { + private ElementReference Ref; + private ElementReference PathRef; + private ElementReference SubPath1Ref; + private ElementReference SubPath2Ref; + + private (double X, double Y) StartNode; + private (double X, double Y) SubStartNode; + private (double X, double Y) EndNode; + private (double X, double Y) SubEndNode; + + private TrajectoryDegree Degree; + + private (double X, double Y) ControlPoint1; + private (double X, double Y) ControlPoint2; + + public CreateCurveEdgeStep CreateStep { get; set; } = CreateCurveEdgeStep.Hidden; + + public async Task Update(double x, double y) + { + if (CreateStep == CreateCurveEdgeStep.CreateSubStartEdge) + { + SubStartNode.X = x; + SubStartNode.Y = y; + } + else if (CreateStep == CreateCurveEdgeStep.FinishCreateSubStartEdge) + { + EndNode.X = x; + EndNode.Y = y; + await CreateTangentPath(); + } + else if (CreateStep == CreateCurveEdgeStep.CreateSubEndEdge) + { + SubEndNode.X = x; + SubEndNode.Y = y; + await CreateTangentPath(); + } + + StateHasChanged(); + } + + public async Task CreateSubEdgeAsync(double x, double y, TrajectoryDegree degree = TrajectoryDegree.Two) + { + Degree = degree; + if (CreateStep == CreateCurveEdgeStep.Hidden) + { + await Visible(); + StartNode.X = x; + StartNode.Y = y; + SubStartNode.X = x; + SubStartNode.Y = y; + CreateStep = CreateCurveEdgeStep.CreateSubStartEdge; + } + else if (CreateStep == CreateCurveEdgeStep.FinishCreateSubStartEdge) + { + EndNode.X = x; + EndNode.Y = y; + SubEndNode.X = x; + SubEndNode.Y = y; + CreateStep = CreateCurveEdgeStep.CreateSubEndEdge; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", SubPath2Ref, "visibility", "visible"); + } + StateHasChanged(); + } + + public async Task FinishSubEdgeAsync(double x, double y) + { + if (CreateStep == CreateCurveEdgeStep.CreateSubStartEdge) + { + SubStartNode.X = x; + SubStartNode.Y = y; + + EndNode.X = x; + EndNode.Y = y; + + CreateStep = CreateCurveEdgeStep.FinishCreateSubStartEdge; + await CreateTangentPath(); + } + else if (CreateStep == CreateCurveEdgeStep.CreateSubEndEdge) CreateStep = CreateCurveEdgeStep.Finish; + } + + public EdgeCreateModel GetEdgeCreateModel() + { + return new() + { + X1 = StartNode.X, + Y1 = StartNode.Y, + X2 = EndNode.X, + Y2 = EndNode.Y, + ControlPoint1X = ControlPoint1.X, + ControlPoint1Y = ControlPoint1.Y, + ControlPoint2X = ControlPoint2.X, + ControlPoint2Y = ControlPoint2.Y, + TrajectoryDegree = Degree, + }; + } + + private async Task CreateTangentPath() + { + var data = $"M {StartNode.X} {StartNode.Y}"; + if(Degree == TrajectoryDegree.Two) + { + var cp = MapEditorHelper.CaculateIntersectionPoint(SubStartNode.X, SubStartNode.Y, StartNode.X, StartNode.Y, EndNode.X, EndNode.Y, SubEndNode.X, SubEndNode.Y); + if (cp is null || CreateStep == CreateCurveEdgeStep.FinishCreateSubStartEdge) cp = MapEditorHelper.CaculateControlPoint(StartNode.X, StartNode.Y, EndNode.X, EndNode.Y, 45, 1 / Math.Sqrt(2)); + data = $"{data} Q {cp.Value.X} {cp.Value.Y} {EndNode.X} {EndNode.Y}"; + ControlPoint1 = (cp.Value.X, cp.Value.Y); + } + else if(Degree == TrajectoryDegree.Three) + { + if (CreateStep == CreateCurveEdgeStep.FinishCreateSubStartEdge) + { + ControlPoint1 = MapEditorHelper.CaculateControlPoint(StartNode.X, StartNode.Y, EndNode.X, EndNode.Y, 45, 1 / Math.Sqrt(2)); + ControlPoint2 = MapEditorHelper.CaculateControlPoint(StartNode.X, StartNode.Y, EndNode.X, EndNode.Y, -45, 1 / Math.Sqrt(2)); + } + else (ControlPoint1.X, ControlPoint1.Y, ControlPoint2.X, ControlPoint2.Y) = MapEditorHelper.CaculateDoubleControlPoint(SubStartNode.X, SubStartNode.Y, StartNode.X, StartNode.Y, EndNode.X, EndNode.Y, SubEndNode.X, SubEndNode.Y); + data = $"{data} C {ControlPoint1.X} {ControlPoint1.Y}, {ControlPoint2.X} {ControlPoint2.Y}, {EndNode.X} {EndNode.Y}"; + } + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", PathRef, "d", data); + StateHasChanged(); + } + + public async Task Visible() + { + EndNode = (0, 0); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", PathRef, "d", ""); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", SubPath1Ref, "visibility", "visible"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", SubPath2Ref, "visibility", "hidden"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "visible"); + } + + public async Task Hidden() + { + CreateStep = CreateCurveEdgeStep.Hidden; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", SubPath1Ref, "visibility", "hidden"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", SubPath2Ref, "visibility", "hidden"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "hidden"); + } + + public enum CreateCurveEdgeStep + { + Hidden, + CreateSubStartEdge, + FinishCreateSubStartEdge, + CreateSubEndEdge, + Finish, + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor.css new file mode 100644 index 0000000..e0a476e --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeCurveCreating.razor.css @@ -0,0 +1,12 @@ +path { + stroke: red; + stroke-width: var(--edge-stroke-width); + fill: none; +} + +circle { + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor new file mode 100644 index 0000000..08a6668 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor @@ -0,0 +1,113 @@ +@implements IDisposable + +@inject IJSRuntime JSRuntime + + + + +@code { + [Parameter, EditorRequired] + public EdgeModel Model { get; set; } = null!; + + private ElementReference Ref; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await UpdatePathData(); + await UpdateMaker(); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateLine = false; + if (parameters.TryGetValue(nameof(Model), out EdgeModel? model)) + { + if (((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) + { + Model.StartNodePositionChanged -= UpdatePathData; + Model.EndNodePositionChanged -= UpdatePathData; + Model.ControlPoint1PositionChanged -= UpdatePathData; + Model.ControlPoint2PositionChanged -= UpdatePathData; + Model.DirectionChanged -= UpdateMaker; + } + + if (model != null) + { + model.StartNodePositionChanged += UpdatePathData; + model.EndNodePositionChanged += UpdatePathData; + model.ControlPoint1PositionChanged += UpdatePathData; + model.ControlPoint2PositionChanged += UpdatePathData; + model.DirectionChanged += UpdateMaker; + updateLine = true; + } + } + } + + await base.SetParametersAsync(parameters); + if (updateLine) + { + await UpdatePathData(); + await UpdateMaker(); + } + } + + private async Task UpdatePathData() + { + string data = string.Empty; + double sx = 0, sy = 0, mx = 0, my = 0, ex = 0, ey = 0; + double sTime = 0.49, eTime = 0.5; + + + if (Model.TrajectoryDegree == TrajectoryDegree.One) + { + sx = Model.X1; + sy = Model.Y1; + ex = Model.X2; + ey = Model.Y2; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Two) + { + sx = (Model.X1 + Model.ControlPoint1X) / 2; + sy = (Model.Y1 + Model.ControlPoint1Y) / 2; + ex = (Model.X2 + Model.ControlPoint1X) / 2; + ey = (Model.Y2 + Model.ControlPoint1Y) / 2; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Three) + { + sx = Math.Pow(1 - sTime, 3) * Model.X1 + 3 * Math.Pow(1 - sTime, 2) * sTime * Model.ControlPoint1X + 3 * Math.Pow(sTime, 2) * (1 - sTime) * Model.ControlPoint2X + Math.Pow(sTime, 3) * Model.X2; + sy = Math.Pow(1 - sTime, 3) * Model.Y1 + 3 * Math.Pow(1 - sTime, 2) * sTime * Model.ControlPoint1Y + 3 * Math.Pow(sTime, 2) * (1 - sTime) * Model.ControlPoint2Y + Math.Pow(sTime, 3) * Model.Y2; + + ex = Math.Pow(1 - eTime, 3) * Model.X1 + 3 * Math.Pow(1 - eTime, 2) * eTime * Model.ControlPoint1X + 3 * Math.Pow(eTime, 2) * (1 - eTime) * Model.ControlPoint2X + Math.Pow(eTime, 3) * Model.X2; + ey = Math.Pow(1 - eTime, 3) * Model.Y1 + 3 * Math.Pow(1 - eTime, 2) * eTime * Model.ControlPoint1Y + 3 * Math.Pow(eTime, 2) * (1 - eTime) * Model.ControlPoint2Y + Math.Pow(eTime, 3) * Model.Y2; + } + + mx = (sx + ex) / 2; + my = (sy + ey) / 2; + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", $"{sx},{sy} {mx},{my} {ex},{ey}"); + } + + private async Task UpdateMaker() + { + if (Model == null) return; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "marker-mid", MapSvgDefs.GetMakerMid(Model.DirectionAllowed)); + } + + public void Dispose() + { + if (Model != null) + { + Model.StartNodePositionChanged -= UpdatePathData; + Model.EndNodePositionChanged -= UpdatePathData; + Model.ControlPoint1PositionChanged -= UpdatePathData; + Model.ControlPoint2PositionChanged -= UpdatePathData; + Model.DirectionChanged -= UpdateMaker; + } + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor.css new file mode 100644 index 0000000..08b21eb --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeDirection.razor.css @@ -0,0 +1,5 @@ +polyline { + stroke-width: var(--edge-direction-stroke-width); + stroke: none; + fill: none; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor new file mode 100644 index 0000000..eb9986d --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor @@ -0,0 +1,46 @@ +@inject IJSRuntime JSRuntime + + + + + + + +@code { + private ElementReference Ref; + private ElementReference PathRef; + + public double X1; + public double Y1; + public double X2; + public double Y2; + + + + public async Task Update(double x, double y) + { + X2 = x; + Y2 = y; + await CaculatePath(); + } + + public async Task StartCreateAsync(double x, double y) + { + X1 = x; + Y1 = y; + X2 = x; + Y2 = y; + await CaculatePath(); + await Visible(); + } + + private async Task CaculatePath() + { + var data = $"M {X1} {Y1} L {(X1 + X2) / 2} {(Y1 + Y2) / 2} L {X2} {Y2}"; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", PathRef, "d", data); + StateHasChanged(); + } + + public async Task Visible() => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "visible"); + public async Task Hidden() => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "hidden"); +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor.css new file mode 100644 index 0000000..50737be --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/EdgeStraightCreating.razor.css @@ -0,0 +1,12 @@ +path { + stroke: red; + stroke-width: var(--edge-stroke-width); + fill: none; +} + +circle { + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor b/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor new file mode 100644 index 0000000..0715d62 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor @@ -0,0 +1,255 @@ +@implements IDisposable +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + + + @foreach (var edge in Models) + { + + } + @foreach (var edge in Models) + { + + } + + + + + + Update edge @UpdateModel.Id + + + +
+ + Length: @SelectModelLength m + +
+
+
+
+ +
+
+ +
+
+ + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + @foreach (var action in Actions) + { + @action.Name + } + + +
+
+ @foreach (var actionId in UpdateModel.Actions) + { +
+ + @(Actions.FirstOrDefault(ac => ac.Id == actionId)?.Name) + +
+ } +
+
+
+
+ + Cancel + Update + +
+ +@code { + [Parameter, EditorRequired] + public MapEdgeModel Models { get; set; } = null!; + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public EventCallback EdgeSelectedChanged { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + private EdgeUpdateModel UpdateModel = new(); + private bool updateEdgeVisible; + private string ActionsText { get; set; } = ""; + private double SelectModelLength; + + private List Actions = []; + private ActionDto? ActionSelected = null; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + Models.Changed += StateHasChanged; + Models.EdgeSelectedChanged += async (model) => await EdgeSelectedChanged.InvokeAsync(model); + } + + private async Task LoadActionAsync(Guid mapVersionId) + { + var result = await Http.GetFromJsonAsync>($"api/Actions/{mapVersionId}"); + + if (result is not null && result.Any()) + { + Actions.Clear(); + Actions.AddRange(result); + } + if (Actions.Any()) ActionSelected = Actions.First(); + + StateHasChanged(); + } + + private void AddAction() + { + if (ActionSelected is null) return; + if (UpdateModel.Actions.Any(action => action == ActionSelected.Id)) + { + Snackbar.Add("Action đã tồn tại", Severity.Warning); + return; + } + + UpdateModel.Actions = [.. UpdateModel.Actions, ActionSelected.Id]; + StateHasChanged(); + } + + private void DeleteAction(Guid actionId) + { + UpdateModel.Actions = UpdateModel.Actions.Where(a => actionId != a).ToArray(); + StateHasChanged(); + } + + private async Task OnEdgeDoubleClick(EdgeModel model) + { + if (model == null || EditorState != EditorState.NavigationEdit) return; + + await LoadActionAsync(model.MapId); + + UpdateModel.Id = model.Id; + UpdateModel.ControlPoint1X = model.ControlPoint1X; + UpdateModel.ControlPoint1Y = model.ControlPoint1Y; + UpdateModel.ControlPoint2X = model.ControlPoint2X; + UpdateModel.ControlPoint2Y = model.ControlPoint2Y; + UpdateModel.DirectionAllowed = model.DirectionAllowed; + UpdateModel.MaxSpeed = model.MaxSpeed; + UpdateModel.MaxHeight = model.MaxHeight; + UpdateModel.MinHeight = model.MinHeight; + UpdateModel.RotationAllowed = model.RotationAllowed; + UpdateModel.MaxRotationSpeed = model.MaxRotationSpeed; + UpdateModel.AllowedDeviationXy = model.AllowedDeviationXy; + UpdateModel.AllowedDeviationTheta = model.AllowedDeviationTheta; + + UpdateModel.Actions = []; + var actions = JsonSerializer.Deserialize(model.Actions); + if (actions is not null && actions.Length > 0) + { + UpdateModel.Actions = [.. actions]; + } + + SelectModelLength = MapEditorHelper.GetEdgeLength(new() + { + TrajectoryDegree = model.TrajectoryDegree, + X1 = model.X1, + Y1 = model.Y1, + X2 = model.X2, + Y2 = model.Y2, + ControlPoint1X = model.ControlPoint1X, + ControlPoint1Y = model.ControlPoint1Y, + ControlPoint2X = model.ControlPoint2X, + ControlPoint2Y = model.ControlPoint2Y, + }); + + updateEdgeVisible = true; + StateHasChanged(); + } + + private void CancelUpdateEdge() + { + updateEdgeVisible = false; + StateHasChanged(); + } + + private async Task UpdateEdge() + { + var selectedModel = Models.FirstOrDefault(e => e.Id == UpdateModel.Id); + + if (selectedModel == null) + { + updateEdgeVisible = false; + StateHasChanged(); + return; + } + + var result = await (await Http.PutAsJsonAsync($"api/Edges", UpdateModel)).Content.ReadFromJsonAsync(); + if (result is null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + selectedModel.UpdateData(UpdateModel); + updateEdgeVisible = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + + public void Dispose() + { + Models.Changed -= StateHasChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor.css new file mode 100644 index 0000000..6f4d3db --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Edge/MapEdge.razor.css @@ -0,0 +1,13 @@ +.paper-action { + padding: 5px; + border: 1px solid silver; + border-radius: 10px; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + overflow-x: hidden; + overflow-y: auto; + width: 280px; + max-height: 288px; + height: 100%; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor b/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor new file mode 100644 index 0000000..1a71491 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor @@ -0,0 +1,113 @@ +@implements IDisposable + +@inject IHttpClientFactory HttpClientFactory +@inject IJSRuntime JSRuntime + + + + @Model?.Name + + +@code { + [Parameter, EditorRequired] + public ElementModel Model { get; set; } = null!; + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + [Parameter] + public EventCallback DoubleClick { get; set; } + + private ElementReference ImageRef; + private ElementReference textRef; + private ElementModelDto ElementModel = new(); + + private bool IsSetting => EditorState == EditorState.View; + private bool IsShow => EditorState == EditorState.View; + private HttpClient? Http; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + var model = await Http.GetFromJsonAsync>($"api/ElementModels/{Model.ModelId}"); + if (model is not null && model.Data is not null) + { + await JSRuntime.InvokeVoidAsync("SetImageAttribute", ImageRef, model.Data.Width, model.Data.Height, -model.Data.Width / 2, -model.Data.Height / 2, $"{Http.BaseAddress}api/images/elementModel/{model.Data.Id}?IsOpen={Model.IsOpen}"); + ElementModel = model.Data; + } + if (Model is not null) + { + Model.PositionChanged += Model_PositionChanged; + Model.OffsetChanged += Model_OffsetChanged; + await UpdatePosition(Model.X, Model.Y, Model.Theta); + } + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateRobot = false; + if (parameters.TryGetValue(nameof(Model), out ElementModel? model) && ((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) + { + Model.OffsetChanged -= Model_OffsetChanged; + Model.PositionChanged -= Model_PositionChanged; + } + + if (model != null) + { + updateRobot = true; + model.OffsetChanged += Model_OffsetChanged; + model.PositionChanged += Model_PositionChanged; + + } + } + await base.SetParametersAsync(parameters); + if (updateRobot && model != null) + { + if (Http is null) Http = HttpClientFactory.CreateClient("MapManagerAPI"); + var elementModel = await Http.GetFromJsonAsync>($"api/ElementModels/{model.ModelId}"); + if (elementModel is not null && elementModel.Data is not null) ElementModel = elementModel.Data; + await SetElementImage(model.IsOpen); + await UpdatePosition(model.X, model.Y, model.Theta); + } + } + + private async Task SetElementImage(bool isOpen) + { + if (Http is null) Http = HttpClientFactory.CreateClient("MapManagerAPI"); + if(ElementModel is null) + { + var elementModel = await Http.GetFromJsonAsync>($"api/ElementModels/{Model.ModelId}"); + if (elementModel is not null && elementModel.Data is not null) ElementModel = elementModel.Data; + } + if (ElementModel is not null) + { + await JSRuntime.InvokeVoidAsync("SetImageAttribute", ImageRef, ElementModel.Width, ElementModel.Height, (-ElementModel.Width / 2), (-ElementModel.Height / 2), $"{Http.BaseAddress}api/images/elementModel/{ElementModel.Id}?IsOpen={isOpen}"); + } + } + + public async Task UpdatePosition(double x, double y, double theta) + => await JSRuntime.InvokeVoidAsync("SetElementPosition", ImageRef, textRef, x + Model.OffsetX, y + Model.OffsetY, theta, -ElementModel.Width / 2, -ElementModel.Height / 2); + public async Task UpdatePosition(double offsetX, double offsetY) + => await JSRuntime.InvokeVoidAsync("SetElementPosition", ImageRef, textRef, Model.X + offsetX, Model.Y + offsetY, Model.Theta, -ElementModel.Width / 2, -ElementModel.Height / 2); + + private async Task Model_PositionChanged() => await UpdatePosition(Model.X, Model.Y, Model.Theta); + private async Task Model_OffsetChanged() => await UpdatePosition(Model.OffsetX, Model.OffsetY); + + public void Dispose() + { + if (Model != null) + { + Model.OffsetChanged -= Model_OffsetChanged; + Model.PositionChanged -= Model_PositionChanged; + } + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor.css new file mode 100644 index 0000000..4faa803 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Element/Element.razor.css @@ -0,0 +1,21 @@ +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + font-size: 0.01em; + user-select: none; + font-weight: normal; +} + +.element { + transform-origin: center; + transform-box: fill-box; +} + + .element:hover { + cursor: pointer; + transform: scale(1.2); + filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.3)); + transition: all 0.5s ease; + } \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor new file mode 100644 index 0000000..2e91852 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor @@ -0,0 +1,558 @@ +@using RobotNet.MapShares.Models +@using RobotNet.WebApp.Maps.Components.Editor.Edge +@using RobotNet.WebApp.Maps.Components.Editor.Node +@using RobotNet.WebApp.Maps.Components.Editor.Zone + +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory +@inject IDialogService Dialog +@inject ISnackbar Snackbar + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + + @OverlayIsStr + +
+ +@code { + [Parameter] + public bool ShowGrid { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public bool ShowMapSlam { get; set; } + + [Parameter] + public bool MapIsActive { get; set; } + + [Parameter] + public ZoneType ZoneType { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + [Parameter] + public EventCallback MultiselectedEdgeChanged { get; set; } + + [Parameter] + public EventCallback MultiselectedNodeChanged { get; set; } + + [Parameter] + public EventCallback ZoneselectedChanged { get; set; } + + [Parameter] + public EventCallback NodesUndoableChanged { get; set; } + + [Parameter] + public EventCallback KeyPress { get; set; } + + [Parameter] + public EventCallback MapIsChecking { get; set; } + + private MapMousePosition MapInfoRef = null!; + private EdgeStraightCreating EdgeStraightCreatingRef = null!; + private EdgeCurveCreating EdgeCurveCreatingRef = null!; + private MapEdge MapEdgesRef = null!; + private MapGrid MapGridRef = null!; + private EdgeControlPoint EdgeControlPointRef = null!; + private MapScaner MapScanerRef = null!; + private ZoneCreating ZoneCreatingRef = null!; + private MapZone MapZoneRef = null!; + private ZoneControlPoint ZoneControlPointRef = null!; + private MapCopy MapCopyRef = null!; + + private DotNetObjectReference DotNetObj = null!; + private ElementReference ViewContainerRef; + private ElementReference ViewMovementRef; + private ElementReference MapContainerRef; + private ElementReference MapImageRef; + + private double ViewContainerRectX; + private double ViewContainerRectY; + private double ViewContainerRectWidth; + private double ViewContainerRectHeight; + private double ViewContainerRectTop; + private double ViewContainerRectRight; + private double ViewContainerRectBottom; + private double ViewContainerRectLeft; + + private double CursorX; + private double CursorY; + private double ClientOriginX; + private double ClientOriginY; + private double Scale = 1; + private double Left; + private double Top; + private double FitScale = 1; + + private double Resolution = 1; + private double OriginX; + private double OriginY; + private double ImageWidth; + private double ImageHeight; + + private MapDataDto MapData = new(); + public MapEdgeModel Edges { get; set; } = []; + public MapNodeModel Nodes { get; set; } = []; + public MapZoneModel Zones { get; set; } = []; + public MapElementModel Elements { get; set; } = []; + private HttpClient Http = default!; + + private bool OverlayIsVisible = false; + private string OverlayIsStr = "Loading..."; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + DotNetObj = DotNetObjectReference.Create(this); + + await JSRuntime.InvokeVoidAsync("DOMCssLoaded"); + + await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await JSRuntime.InvokeVoidAsync("ResizeObserverRegister", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await JSRuntime.InvokeVoidAsync("AddMouseMoveEventListener", DotNetObj, ViewContainerRef, nameof(MouseMoveOnMapContainer)); + await JSRuntime.InvokeVoidAsync("AddEventListener", DotNetObj, ViewContainerRef, "click", nameof(ViewContainerClick)); + await JSRuntime.InvokeVoidAsync("AddKeyUpEventListener", DotNetObj, ViewContainerRef, nameof(ViewContainerKeyUp)); + + await JSRuntime.InvokeVoidAsync("AddMouseWheelEventListener", DotNetObj, MapContainerRef, nameof(MouseWheelOnMapContainer)); + await JSRuntime.InvokeVoidAsync("AddMouseUpEventListener", DotNetObj, MapContainerRef, nameof(MouseUpOnMapContainer)); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, MapContainerRef, nameof(MouseDownOnMapContainer)); + await JSRuntime.InvokeVoidAsync("AddTouchMoveEventListener", DotNetObj, ViewContainerRef, nameof(TouchMoveOnMapContainer)); + } + + private async Task LoadMap() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + var mapDataResult = await Http.GetFromJsonAsync>($"api/MapsData/{MapData.Id}"); + if (mapDataResult is not null && mapDataResult.Data is not null) + { + MapIsActive = mapDataResult.Data.Active; + await LoadMap(mapDataResult.Data); + OverlayIsVisible = false; + } + else OverlayIsStr = "Map Not Existed"; + StateHasChanged(); + } + catch + { + OverlayIsStr = "Map Not Existed"; + StateHasChanged(); + return; + } + } + + public async Task LoadMap(MapDataDto? mapData) + { + if (mapData == null) + { + MapData = new(); + Nodes.ReplaceAll([]); + Edges.ReplaceAll([]); + Zones.ReplaceAll([]); + Elements.ReplaceAll([]); + Resolution = 1.0; + OriginY = 0; + OriginX = 0; + ImageHeight = 0; + ImageWidth = 0; + MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth); + await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, 1.0); + await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, ""); + } + else + { + MapData = mapData; + Nodes.ReplaceAll(MapData.Nodes); + Edges.ReplaceAll(MapData.Edges.Select(edge => new EdgeModel(edge, Nodes[edge.StartNodeId], Nodes[edge.EndNodeId]))); + Zones.ReplaceAll(MapData.Zones); + Elements.ReplaceAll(MapData.Elements.Select(element => new ElementModel(element, Nodes[element.NodeId]))); + Resolution = mapData.Resolution; + OriginY = -mapData.ImageHeight * Resolution - mapData.OriginY; + OriginX = mapData.OriginX; + ImageHeight = mapData.ImageHeight * Resolution; + ImageWidth = mapData.ImageWidth * Resolution; + MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth); + + await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, mapData.OriginY); + await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, $"{Http.BaseAddress}api/images/map/{mapData.Id}"); + await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await ScaleFitContentAsync(); + } + } + + public async Task ScaleFitContentAsync() + { + Scale = FitScale; + await SetViewMovement((ViewContainerRectWidth - ImageWidth * Scale) / 2, (ViewContainerRectHeight - ImageHeight * Scale) / 2); + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + } + + private async Task SetViewMovement(double left, double top) + { + Top = top; + Left = left; + + ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale; + ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale; + await JSRuntime.InvokeVoidAsync("SetMapMovement", ViewMovementRef, Top, Left); + } + + public async Task ScaleZoom(double deltaY) + { + if (deltaY > 0) + { + if (Scale >= FitScale * 20) return; + } + else + { + if (Scale <= FitScale / 2) return; + } + double oldScale = Scale; + Scale += deltaY; + + double centerXBefore = ((ViewContainerRectLeft + ViewContainerRectWidth / 2) - ClientOriginX) / oldScale - OriginX; + double centerYBefore = (ClientOriginY - (ViewContainerRectHeight / 2 + ViewContainerRectTop)) / oldScale - (MapData is null ? 0 : MapData.OriginY); + + await SetViewMovement(Left - centerXBefore * deltaY, Top - (ImageHeight - centerYBefore) * deltaY); + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + } + + [JSInvokable] + public async Task ViewContainerClick() => await ViewContainerRef.FocusAsync(); + + [JSInvokable] + public void ViewContainerResize(double x, double y, double width, double height, double top, double right, double bottom, double left) + { + ViewContainerRectX = x; + ViewContainerRectY = y; + ViewContainerRectWidth = width; + ViewContainerRectHeight = height; + ViewContainerRectTop = top; + ViewContainerRectRight = right; + ViewContainerRectBottom = bottom; + ViewContainerRectLeft = left; + + ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale; + ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale; + + FitScale = Math.Min(ViewContainerRectWidth / ImageWidth, ViewContainerRectHeight / ImageHeight); + } + + [JSInvokable] + public async Task TouchMoveOnMapContainer(double clientX, double clientY, int touchCount, double movementX, double movementY) + { + CursorX = (clientX - ClientOriginX) / Scale; + CursorY = (ClientOriginY - clientY) / Scale; + MapInfoRef.Update(CursorX, CursorY); + + if (touchCount == 1) await SetViewMovement(Left + movementX, Top + movementY); + } + + [JSInvokable] + public async Task MouseWheelOnMapContainer(double deltaY, double offsetX, double offsetY) + { + double scaleChange; + if (deltaY > 0) + { + if (Scale <= FitScale / 2) return; + scaleChange = Scale > FitScale ? -(Scale / FitScale) : -0.1; + } + else + { + if (Scale >= FitScale * 100) return; + scaleChange = Scale < FitScale ? 0.5 : (Scale / FitScale); + } + + double oldScale = Scale; + + Scale += scaleChange; + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + + double mouseX = CursorX - OriginX; + double mouseY = CursorY - (MapData is null ? 0 : MapData.OriginY); + await SetViewMovement(Left - mouseX * scaleChange, Top - (ImageHeight - mouseY) * scaleChange); + } + + [JSInvokable] + public async Task MouseMoveOnMapContainer(double clientX, double clientY, long buttons, bool ctrlKey, double movementX, double movementY) + { + CursorX = (clientX - ClientOriginX) / Scale; + CursorY = (ClientOriginY - clientY) / Scale; + MapInfoRef.Update(CursorX, CursorY); + if (EditorState == EditorState.CreateCurveEdge || EditorState == EditorState.CreateDoubleCurveEdge) await EdgeCurveCreatingRef.Update(CursorX, CursorY); + else if (EditorState == EditorState.CreateZone) await ZoneCreatingRef.Update(CursorX, CursorY); + + switch (buttons) + { + case 1: + if (MapIsActive) break; + switch (EditorState) + { + case EditorState.CreateStraighEdge: + await EdgeStraightCreatingRef.Update(CursorX, CursorY); + break; + case EditorState.CreateCurveEdge: + case EditorState.CreateDoubleCurveEdge: + await EdgeCurveCreatingRef.Update(CursorX, CursorY); + break; + case EditorState.Scaner: + await MapScanerRef.Update(CursorX, CursorY); + break; + case EditorState.NavigationEdit: + if (ctrlKey) NodePositionMove(CursorX, CursorY); + break; + case EditorState.SettingZone: + if (ctrlKey) ZoneUpdateShape(CursorX, CursorY); + break; + case EditorState.Move: + if (ctrlKey) + { + if (Edges.ActivedEdges.Count > 0) EdgesPositionMove(CursorX, CursorY); + else NodesPositionMove(CursorX, CursorY); + } + break; + case EditorState.Copy: + if (ctrlKey) MapCopyRef.UpdateMove(CursorX, CursorY); + break; + } + break; + case 4: + await SetViewMovement(Left + movementX, Top + movementY); + break; + } + } + + [JSInvokable] + public async Task ViewContainerKeyUp(string code, string key, bool altKey, bool ctrlKey, bool shiftKey) + { + if (MapIsActive) return; + switch (code) + { + case "Escape": + await EditStateChange(); + break; + case "Delete": + _ = InvokeAsync(async Task () => + { + if (EditorState == EditorState.NavigationEdit || EditorState == EditorState.Scaner) await DeleteEdge(); + else if (EditorState == EditorState.SettingZone) await DeleteZone(); + }); + break; + case "KeyZ": + if (ctrlKey) UndoEditorBackup(); + break; + case "KeyS": + if (ctrlKey) await SaveChanged(); + break; + case "KeyM": + if (EditorState != EditorState.Scaner) return; + if (ctrlKey) + { + if (EditorBackup.Count > 0) + { + var save = await SaveChanged(); + if (!save) break; + } + await SetCursor("grab"); + await KeyPress.InvokeAsync(EditorState.Move); + } + break; + case "KeyC": + if (EditorState != EditorState.Scaner) return; + if (ctrlKey) await KeyPress.InvokeAsync(EditorState.Copy); + break; + } + StateHasChanged(); + } + + [JSInvokable] + public async Task MouseUpOnMapContainer(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (button == 0 && !MapIsActive) + { + switch (EditorState) + { + case EditorState.CreateStraighEdge: + await CreateEdge(EdgeStraightCreatingRef.X1, EdgeStraightCreatingRef.Y1, EdgeStraightCreatingRef.X2, EdgeStraightCreatingRef.Y2); + await EdgeStraightCreatingRef.Hidden(); + break; + case EditorState.CreateCurveEdge: + case EditorState.CreateDoubleCurveEdge: + await EdgeCurveCreatingRef.FinishSubEdgeAsync(CursorX, CursorY); + if (EdgeCurveCreatingRef.CreateStep == EdgeCurveCreating.CreateCurveEdgeStep.Finish) + { + var edgeCreateModel = EdgeCurveCreatingRef.GetEdgeCreateModel(); + await CreateEdge(edgeCreateModel.X1, edgeCreateModel.Y1, edgeCreateModel.X2, edgeCreateModel.Y2, edgeCreateModel.TrajectoryDegree, edgeCreateModel.ControlPoint1X, edgeCreateModel.ControlPoint1Y, edgeCreateModel.ControlPoint2X, edgeCreateModel.ControlPoint2Y); + await EdgeCurveCreatingRef.Hidden(); + } + break; + case EditorState.Scaner: + await ScanerActive(MapScanerRef.X1, MapScanerRef.Y1, MapScanerRef.X2, MapScanerRef.Y2); + await MapScanerRef.Hidden(); + break; + case EditorState.CreateZone: + if (ZoneCreatingRef.Step == 4) + { + await CreateZone(ZoneCreatingRef.X1, ZoneCreatingRef.Y1, ZoneCreatingRef.X2, ZoneCreatingRef.Y2, ZoneCreatingRef.X3, ZoneCreatingRef.Y3, ZoneCreatingRef.X4, ZoneCreatingRef.Y4, ZoneType); + await ZoneCreatingRef.Hidden(); + } + break; + case EditorState.SettingZone: + if (Zones.ZoneActived is not null && Zones.ZoneActived.ActiveNode != 5) Zones.ZoneActived.ActiveNode = 5; + break; + case EditorState.NavigationEdit: + if (Edges.ActivedEdges.Count == 1) + { + Edges.ActivedEdges[0].ActivedControlPoint1 = false; + Edges.ActivedEdges[0].ActivedControlPoint2 = false; + } + Nodes.SelectedNode = null; + break; + case EditorState.Move: + await SetCursor("grab"); + break; + } + StateHasChanged(); + } + } + + [JSInvokable] + public async Task MouseDownOnMapContainer(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (button == 0 && !MapIsActive) + { + var startNode = MapEditorHelper.GetClosesNode(CursorX, CursorY, Nodes.Select(node => new NodeDto() { X = node.X, Y = node.Y }).ToList()); + switch (EditorState) + { + case EditorState.CreateStraighEdge: + await EdgeStraightCreatingRef.StartCreateAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY); + break; + case EditorState.CreateCurveEdge: + await EdgeCurveCreatingRef.CreateSubEdgeAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY); + break; + case EditorState.CreateDoubleCurveEdge: + await EdgeCurveCreatingRef.CreateSubEdgeAsync(startNode?.X ?? CursorX, startNode?.Y ?? CursorY, TrajectoryDegree.Three); + break; + case EditorState.Scaner: + await MapScanerRef.CreateAsync(CursorX, CursorY); + break; + case EditorState.CreateZone: + await ZoneCreatingRef.CreateAsync(CursorX, CursorY); + break; + case EditorState.SettingZone: + if (Zones.ZoneActived is not null) Zones.ZoneActived.SetStartMovePosition(CursorX, CursorY); + break; + case EditorState.Move: + await SetCursor("grabbing"); + Edges.SetStartMovePosition(CursorX, CursorY); + Nodes.SetStartMovePosition(CursorX, CursorY); + break; + case EditorState.Copy: + MapCopyRef.SetStartMovePosition(CursorX, CursorY); + break; + } + } + } + + private async Task OnSelectedEdgeChanged(EdgeModel? model) + { + if (model is not null && EditorState == EditorState.NavigationEdit) + { + if (model.TrajectoryDegree == TrajectoryDegree.One) EdgeControlPointRef.SetControl(null); + else EdgeControlPointRef.SetControl(model); + Edges.ActivedEdge([model]); + await MultiselectedEdgeChanged.InvokeAsync(Edges.ActivedEdges.Count > 0); + } + else EdgeControlPointRef.SetControl(null); + } + + private void OnActivedZoneChanged((ZoneModel model, bool state) data) + { + if (EditorState == EditorState.SettingZone) + { + if (data.state) + { + ZoneControlPointRef.SetControl(data.model); + _ = ZoneselectedChanged.InvokeAsync(true); + return; + } + } + ZoneControlPointRef.SetControl(null); + } + + public async Task EditStateChange() + { + await EdgeCurveCreatingRef.Hidden(); + await EdgeStraightCreatingRef.Hidden(); + await ZoneCreatingRef.Hidden(); + MapCopyRef.OnShow(false, []); + Zones.UnActivedZone(); + EdgeControlPointRef.SetControl(null); + if (EditorState == EditorState.Copy) await CreateMapCopy(); + if (EditorState != EditorState.Move) + { + Edges.UnActivedEdge(); + Nodes.UnActivedNode(); + } + if (EditorState != EditorState.Copy) await SaveChanged(); + if (EditorState != EditorState.Copy && EditorState != EditorState.Move) await SetCursor("initial"); + await MultiselectedEdgeChanged.InvokeAsync(Edges.ActivedEdges.Count() > 0); + await MultiselectedNodeChanged.InvokeAsync(Nodes.ActivedNodes.Count() > 0); + await ZoneselectedChanged.InvokeAsync(false); + StateHasChanged(); + } + + public async Task CreateMapCopy() + { + if (Edges.ActivedEdges.Count > 0) + { + if (EditorBackup.Count > 0) + { + var save = await SaveChanged(); + if (!save) return; + } + MapCopyRef.OnShow(true, Edges.ActivedEdges); + EditorBackup.Add(new() + { + Type = MapEditorBackupType.Copy, + }); + await NodesUndoableChanged.InvokeAsync(true); + await SetCursor("copy"); + } + } + + public async Task SetCursor(string data) => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", MapContainerRef, "cursor", data); +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.cs b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.cs new file mode 100644 index 0000000..594d648 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.cs @@ -0,0 +1,1092 @@ +using MudBlazor; +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using RobotNet.MapShares.Models; +using RobotNet.Shares; +using RobotNet.WebApp.Components; +using RobotNet.WebApp.Maps.Models; +using System.Net.Http.Json; + +namespace RobotNet.WebApp.Maps.Components.Editor; + +public class MergeNodeBackup +{ + public Guid NodeId { get; set; } + public List<(Guid edgeId, NodeModel node)> EdgesMerge { get; set; } = []; +} + +public partial class MapContainer +{ + public readonly List EditorBackup = []; + + public async Task CreateEdge(double x1, double y1, double x2, double y2, TrajectoryDegree degree = TrajectoryDegree.One, double cpX1 = 0, double cpY1 = 0, double cpX2 = 0, double cpY2 = 0) + { + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return; } + + var result = await (await Http.PostAsJsonAsync($"api/Edges", new EdgeCreateModel() + { + MapId = MapData.Id, + X1 = x1, + Y1 = y1, + X2 = x2, + Y2 = y2, + TrajectoryDegree = degree, + ControlPoint1X = cpX1, + ControlPoint1Y = cpY1, + ControlPoint2X = cpX2, + ControlPoint2Y = cpY2, + })).Content.ReadFromJsonAsync>(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Tạo Edge không thành công", Severity.Error); return; } + else if (result.Data == null || !result.Data.EdgesDto.Any() || result.Data.EdgesDto.Any(e => e.StartNode is null || e.EndNode is null)) + { + Snackbar.Add("Lỗi dữ liệu trả về", Severity.Error); + return; + } + + foreach (var edge in result.Data.EdgesDto) + { + if (edge.StartNode is null || edge.EndNode is null) continue; + if (!Nodes.ContainsKey(edge.StartNodeId)) Nodes.Add(edge.StartNode); + if (!Nodes.ContainsKey(edge.EndNodeId)) Nodes.Add(edge.EndNode); + + Edges.Add(edge, Nodes[edge.StartNodeId], Nodes[edge.EndNodeId]); + } + + foreach (var removeEdgeId in result.Data.RemoveEdge) + { + var removeEdge = Edges.FirstOrDefault(e => e.Id == removeEdgeId); + if (removeEdge is not null) + { + Edges.Remove(removeEdge); + var edgeEditSteps = EditorBackup.Where(step => (step.Type == MapEditorBackupType.ControlPoint1Edge || step.Type == MapEditorBackupType.ControlPoint2Edge) && step.Id == removeEdge.Id); + } + } + Snackbar.Add("Tạo Edge mới thành công", Severity.Success); + } + + public async Task DeleteEdge() + { + if (EditorBackup.Count > 0) + { + if (!await SaveChanged()) return; + } + + if (Edges.ActivedEdges.Count == 0) return; + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể xóa edge do bản đồ đang được Active", Severity.Warning); return; } + + var parameters = new DialogParameters + { + { x => x.Content, "Bạn chắc chắn muốn xóa edge đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var Confirm = await Dialog.ShowAsync("Xoá Edge", parameters); + var ConfirmResult = await Confirm.Result; + if (ConfirmResult is not null && ConfirmResult.Data is not null && bool.TryParse(ConfirmResult.Data.ToString(), out bool data) && data) + { + OverlayIsVisible = true; + StateHasChanged(); + + var deleteListEdgeId = Edges.ActivedEdges.Select(x => x.Id).ToList(); + HttpRequestMessage request = new() + { + Content = JsonContent.Create(deleteListEdgeId), + Method = HttpMethod.Delete, + RequestUri = new Uri($"{Http.BaseAddress}api/Edges") + }; + var response = await Http.SendAsync(request); + var result = await response.Content.ReadFromJsonAsync(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Xóa Edge không thành công", Severity.Error); return; } + + if (Edges.ActivedEdges.Count < 20) + { + List ActivedEdgeCopy = [.. Edges.ActivedEdges]; + Nodes.UnActivedNode(); + Edges.UnActivedEdge(); + foreach (var edge in ActivedEdgeCopy) + { + var removeEdge = Edges.FirstOrDefault(e => e.Id == edge.Id); + if (removeEdge == null) continue; + Edges.Remove(removeEdge); + var edgeEditSteps = EditorBackup.Where(step => (step.Type == MapEditorBackupType.ControlPoint1Edge || step.Type == MapEditorBackupType.ControlPoint2Edge) && step.Id == removeEdge.Id); + for (int i = 0; i < edgeEditSteps.Count(); i++) EditorBackup.Remove(edgeEditSteps.ElementAt(i)); + + if (removeEdge.StartNode.NumberOfEdgeReference <= 1) + { + Nodes.Remove(removeEdge.StartNode); + var element = Elements.FirstOrDefault(el => el.NodeId == removeEdge.StartNode.Id); + if (element is not null) Elements.Remove(element); + var nodeEditSteps = EditorBackup.Where(step => step.Type == MapEditorBackupType.Node && step.Id == removeEdge.StartNode.Id); + for (int i = 0; i < nodeEditSteps.Count(); i++) EditorBackup.Remove(nodeEditSteps.ElementAt(i)); + } + + if (removeEdge.EndNode.NumberOfEdgeReference <= 1) + { + Nodes.Remove(removeEdge.EndNode); + var element = Elements.FirstOrDefault(el => el.NodeId == removeEdge.EndNode.Id); + if (element is not null) Elements.Remove(element); + var nodeEditSteps = EditorBackup.Where(step => step.Type == MapEditorBackupType.Node && step.Id == removeEdge.EndNode.Id); + for (int i = 0; i < nodeEditSteps.Count(); i++) EditorBackup.Remove(nodeEditSteps.ElementAt(i)); + } + } + + await OnSelectedEdgeChanged(null); + await MultiselectedEdgeChanged.InvokeAsync(false); + await MultiselectedNodeChanged.InvokeAsync(false); + + Snackbar.Add("Xóa Edge thành công", Severity.Success); + OverlayIsVisible = false; + } + else await LoadMap(); + StateHasChanged(); + } + + } + + public async Task CreateZone(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, ZoneType type) + { + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return; } + + var result = await (await Http.PostAsJsonAsync($"api/Zones", new ZoneCreateModel() + { + MapId = MapData.Id, + X1 = x1, + Y1 = y1, + X2 = x2, + Y2 = y2, + X3 = x3, + Y3 = y3, + X4 = x4, + Y4 = y4, + Type = type + })).Content.ReadFromJsonAsync>(); + + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Tạo Zone không thành công", Severity.Error); return; } + else if (result.Data is null) { Snackbar.Add("Lỗi dữ liệu trả về", Severity.Error); return; } + + Zones.Add(result.Data); + Snackbar.Add("Tạo Zone mới thành công", Severity.Success); + } + + public async Task DeleteZone() + { + if (EditorBackup.Count > 0) + { + if (!await SaveChanged()) return; + } + + if (Zones.ZoneActived is null) return; + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return; } + + var parameters = new DialogParameters + { + { x => x.Content, "Bạn chắc chắn muốn xóa Zone đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var Confirm = await Dialog.ShowAsync("Xoá Zone", parameters); + var ConfirmResult = await Confirm.Result; + if (ConfirmResult is not null && ConfirmResult.Data is not null && bool.TryParse(ConfirmResult.Data.ToString(), out bool data) && data) + { + + var result = await Http.DeleteFromJsonAsync($"/api/zones/{Zones.ZoneActived.Id}"); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Xoá Zone không thành công", Severity.Error); return; } + + + var removeZone = Zones.ZoneActived; + Zones.Remove(removeZone); + var zoneEditSteps = EditorBackup.Where(step => step.Id == removeZone.Id); + for (int i = 0; i < zoneEditSteps.Count(); i++) EditorBackup.Remove(zoneEditSteps.ElementAt(i)); + + ZoneControlPointRef.SetControl(null); + Snackbar.Add("Xoá Zone thành công", Severity.Success); + } + } + + public async Task SaveChanged() + { + if (EditorBackup.Count == 0) return true; + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return false; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return false; } + + var parameters = new DialogParameters + { + { x => x.Content, "Có dữ liệu thay đổi, bạn có muốn lưu lại không?" }, + { x => x.ConfirmText, "Save" }, + { x => x.Color, Color.Primary } + }; + var Confirm = await Dialog.ShowAsync("Lưu", parameters); + var ConfirmResult = await Confirm.Result; + if (ConfirmResult is not null && ConfirmResult.Data is not null && bool.TryParse(ConfirmResult.Data.ToString(), out bool data) && data) + { + var editBackupSteps = new List(); + foreach (var backup in EditorBackup) + { + switch (backup.Type) + { + case MapEditorBackupType.Node: + if (Nodes.TryGetValue(backup.Id, out var node) && node != null) + { + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.Node, + Id = node.Id, + Obj = new PositionBackup() + { + Id = node.Id, + X = node.X, + Y = node.Y, + } + }); + } + break; + case MapEditorBackupType.ControlPoint1Edge: + if (Edges.TryGetValue(backup.Id, out var edge1) && edge1 is not null) + { + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.ControlPoint1Edge, + Id = edge1.Id, + Obj = new PositionBackup() + { + Id = edge1.Id, + X = edge1.ControlPoint1X, + Y = edge1.ControlPoint1Y, + } + }); + } + break; + case MapEditorBackupType.ControlPoint2Edge: + if (Edges.TryGetValue(backup.Id, out var edge2) && edge2 is not null) + { + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.ControlPoint2Edge, + Id = edge2.Id, + Obj = new PositionBackup() + { + Id = edge2.Id, + X = edge2.ControlPoint2X, + Y = edge2.ControlPoint2Y, + } + }); + } + break; + case MapEditorBackupType.Zone: + if (Zones.TryGetValue(backup.Id, out var zone) && zone is not null) + { + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.Zone, + Id = zone.Id, + Obj = new ZoneShapeBackup() + { + X1 = zone.X1, + Y1 = zone.Y1, + X2 = zone.X2, + Y2 = zone.Y2, + X3 = zone.X3, + Y3 = zone.Y3, + X4 = zone.X4, + Y4 = zone.Y4, + } + }); + } + break; + case MapEditorBackupType.MoveNode: + if (backup.Obj is List nodesChange) + { + foreach (var nodeChange in nodesChange) + { + if (Nodes.TryGetValue(nodeChange.Id, out var nodemove) && nodemove != null) + { + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.Node, + Id = nodemove.Id, + Obj = new PositionBackup() + { + Id = nodemove.Id, + X = nodemove.X, + Y = nodemove.Y, + } + }); + } + } + } + break; + case MapEditorBackupType.MoveEdge: + List saveEdge = []; + if (backup.Obj is List edgesBackup) + { + foreach (var edgeChange in edgesBackup) + { + if (Edges.TryGetValue(edgeChange.Id, out var edge) && edge != null) + { + saveEdge.Add(new() + { + Id = edge.Id, + TrajectoryDegree = edge.TrajectoryDegree, + StartX = edge.X1, + StartY = edge.Y1, + EndX = edge.X2, + EndY = edge.Y2, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y + }); + } + } + } + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.MoveEdge, + Obj = saveEdge, + }); + break; + case MapEditorBackupType.Copy: + if (MapCopyRef.isMoving) + { + List edgeCopyModel = [.. MapCopyRef.EdgeModels.Select(e => new EdgeMapCopyModel() + { + X1 = e.X1, + Y1 = e.Y1, + X2 = e.X2, + Y2 = e.Y2, + MapId = e.MapId, + TrajectoryDegree = e.TrajectoryDegree, + StartNodeId = e.StartNode.Id, + EndNodeId = e.EndNode.Id, + ControlPoint1X = e.ControlPoint1X, + ControlPoint1Y = e.ControlPoint1Y, + ControlPoint2X = e.ControlPoint2X, + ControlPoint2Y = e.ControlPoint2Y, + Actions = e.Actions, + AllowedDeviationTheta = e.AllowedDeviationTheta, + AllowedDeviationXy = e.AllowedDeviationXy, + DirectionAllowed = e.DirectionAllowed, + RotationAllowed = e.RotationAllowed, + MaxHeight = e.MaxHeight, + MinHeight = e.MinHeight, + MaxRotationSpeed = e.MaxRotationSpeed, + MaxSpeed = e.MaxSpeed, + })]; + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.Copy, + Obj = edgeCopyModel, + }); + } + else MapCopyRef.OnShow(false, []); + break; + case MapEditorBackupType.SplitNode: + editBackupSteps.Add(backup); + break; + case MapEditorBackupType.MergeNode: + if (backup.Obj is MergeNodeBackup mergeBackup) + { + Dictionary edgeMerge = []; + mergeBackup.EdgesMerge.ForEach(e => edgeMerge.Add(e.edgeId, e.node.Id)); + editBackupSteps.Add(new() + { + Type = MapEditorBackupType.MergeNode, + Id = mergeBackup.NodeId, + Obj = new MergeNodeUpdate() + { + NodeId = mergeBackup.NodeId, + EdgesMerge = edgeMerge, + } + }); + } + break; + } + + } + + var result = await (await Http.PutAsJsonAsync($"api/MapsData/{MapData.Id}/updates", new MapEditorBackupModel() { Steps = [.. editBackupSteps], })).Content.ReadFromJsonAsync>>(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return false; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Tạo Edge không thành công", Severity.Error); return false; } + else if (result.Data is null) { Snackbar.Add("Lỗi dữ liệu trả về", Severity.Error); return false; } + else if (result.Data.Any()) + { + foreach (var edge in result.Data) + { + if (edge.StartNode is null || edge.EndNode is null) continue; + if (!Nodes.ContainsKey(edge.StartNodeId)) Nodes.Add(edge.StartNode); + if (!Nodes.ContainsKey(edge.EndNodeId)) Nodes.Add(edge.EndNode); + Edges.Add(edge, Nodes[edge.StartNodeId], Nodes[edge.EndNodeId]); + } + MapCopyRef.OnShow(false, []); + } + + EditorBackup.Clear(); + await NodesUndoableChanged.InvokeAsync(false); + Snackbar.Add("Cập nhật thành công", Severity.Success); + return true; + } + return false; + } + + public void UndoEditorBackup() + { + if (EditorBackup.Count == 0 || MapData.Active) return; + + var lastChange = EditorBackup[^1]; + EditorBackup.Remove(lastChange); + + switch (lastChange.Type) + { + case MapEditorBackupType.Node: + if (lastChange.Obj is PositionBackup nodepos) + { + if (Nodes.TryGetValue(lastChange.Id, out var node) && node != null) + { + node.UpdatePosition(nodepos.X, nodepos.Y); + } + } + break; + case MapEditorBackupType.ControlPoint1Edge: + if (lastChange.Obj is PositionBackup cppos) + { + if (Edges.TryGetValue(lastChange.Id, out var edge) && edge != null) + { + edge.UpdateControlPoint1(cppos.X, cppos.Y); + } + } + break; + case MapEditorBackupType.ControlPoint2Edge: + if (lastChange.Obj is PositionBackup cp2pos) + { + if (Edges.TryGetValue(lastChange.Id, out var edge) && edge != null) + { + edge.UpdateControlPoint2(cp2pos.X, cp2pos.Y); + } + } + break; + case MapEditorBackupType.Zone: + if (lastChange.Obj is ZoneShapeBackup zoneshape) + { + if (Zones.TryGetValue(lastChange.Id, out var zone) && zone is not null) + { + zone.UpdateControlNode(zoneshape.X1, zoneshape.Y1, zoneshape.X2, zoneshape.Y2, zoneshape.X3, zoneshape.Y3, zoneshape.X4, zoneshape.Y4); + } + } + break; + case MapEditorBackupType.MoveNode: + if (lastChange.Obj is List nodesbackup) + { + foreach (var node in nodesbackup) + { + if (Nodes.TryGetValue(node.Id, out var model) && model != null) + { + model.UpdatePosition(node.X, node.Y); + } + } + } + break; + case MapEditorBackupType.MoveEdge: + if (lastChange.Obj is List edgesBackup) + { + foreach (var edge in edgesBackup) + { + if (Edges.TryGetValue(edge.Id, out var model) && model != null) + { + model.StartNode.UpdatePosition(edge.StartX, edge.StartY); + model.EndNode.UpdatePosition(edge.EndX, edge.EndY); + if (model.TrajectoryDegree == TrajectoryDegree.Two || model.TrajectoryDegree == TrajectoryDegree.Three) model.UpdateControlPoint1(edge.ControlPoint1X, edge.ControlPoint1Y); + if (model.TrajectoryDegree == TrajectoryDegree.Three) model.UpdateControlPoint2(edge.ControlPoint2X, edge.ControlPoint2Y); + } + } + } + break; + case MapEditorBackupType.Copy: + MapCopyRef.OnShow(false, []); + break; + case MapEditorBackupType.SplitNode: + if (lastChange.Obj is SplitNodeBackup splitBackup) + { + var node = Nodes[splitBackup.NodeId]; + if (node is not null) + { + foreach (var data in splitBackup.EdgeSplit) + { + var edge = Edges[data.Key]; + if (edge is not null && (edge.StartNode.Id == data.Value.Id || edge.EndNode.Id == data.Value.Id)) + { + edge.UpdateNode(data.Value.Id, node); + Nodes.Remove(Nodes[data.Value.Id]); + } + } + } + } + break; + case MapEditorBackupType.MergeNode: + if (lastChange.Obj is MergeNodeBackup mergeBackup) + { + foreach (var (edgeId, node) in mergeBackup.EdgesMerge) + { + var edge = Edges[edgeId]; + if (edge is not null && (edge.StartNode.Id == mergeBackup.NodeId || edge.EndNode.Id == mergeBackup.NodeId)) + { + Nodes.Add(new NodeDto() + { + Id = node.Id, + MapId = node.MapId, + Name = node.Name, + X = node.X, + Y = node.Y, + Theta = node.Theta, + AllowedDeviationXy = node.AllowedDeviationXy, + AllowedDeviationTheta = node.AllowedDeviationTheta, + Actions = node.Actions + }); + edge.UpdateNode(mergeBackup.NodeId, Nodes[node.Id]); + } + } + } + break; + } + + NodesUndoableChanged.InvokeAsync(EditorBackup.Count > 0); + } + + public void HorizontalLeft() + { + if (Nodes.ActivedNodes.Count > 0) + { + List ActiveNode = []; + double MinX = Nodes.ActivedNodes.First().X; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MinX > node.X) MinX = node.X; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + foreach (var node in Nodes.ActivedNodes) + { + node.UpdatePosition(MinX, node.Y); + } + } + } + + public void HorizontalRight() + { + if (Nodes.ActivedNodes.Count > 0) + { + List ActiveNode = []; + double MaxX = Nodes.ActivedNodes.First().X; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MaxX < node.X) MaxX = node.X; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + foreach (var node in Nodes.ActivedNodes) + { + node.UpdatePosition(MaxX, node.Y); + } + } + } + + public void VerticalTop() + { + if (Nodes.ActivedNodes.Count > 0) + { + List ActiveNode = []; + double MaxY = Nodes.ActivedNodes.First().Y; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MaxY < node.Y) MaxY = node.Y; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + foreach (var node in Nodes.ActivedNodes) + { + node.UpdatePosition(node.X, MaxY); + } + } + } + + public void VerticalBottom() + { + if (Nodes.ActivedNodes.Count > 0) + { + List ActiveNode = []; + double MinY = Nodes.ActivedNodes.First().Y; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MinY > node.Y) MinY = node.Y; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + foreach (var node in Nodes.ActivedNodes) + { + node.UpdatePosition(node.X, MinY); + } + } + } + + public void HorizontalCenter() + { + if (Nodes.ActivedNodes.Count > 2) + { + List ActiveNode = []; + double MinX = Nodes.ActivedNodes.First().X; + double MaxX = Nodes.ActivedNodes.First().X; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MinX > node.X) MinX = node.X; + if (MaxX < node.X) MaxX = node.X; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + List NodeSort = [.. Nodes.ActivedNodes.OrderBy(n => n.X)]; + var offset = (MaxX - MinX) / (Nodes.ActivedNodes.Count - 1); + + for (int i = 1; i < NodeSort.Count - 1; i++) + { + var node = Nodes.ActivedNodes.FirstOrDefault(n => n.Id == NodeSort[i].Id); + node?.UpdatePosition(NodeSort[0].X + offset * i, node.Y); + } + } + } + + public void VerticalCenter() + { + if (Nodes.ActivedNodes.Count > 2) + { + List ActiveNode = []; + double MinY = Nodes.ActivedNodes.First().Y; + double MaxY = Nodes.ActivedNodes.First().Y; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = node.X, + Y = node.Y, + }); + if (MinY > node.Y) MinY = node.Y; + if (MaxY < node.Y) MaxY = node.Y; + } + + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + + List NodeSort = [.. Nodes.ActivedNodes.OrderBy(n => n.Y)]; + var offset = (MaxY - MinY) / (Nodes.ActivedNodes.Count - 1); + + for (int i = 1; i < NodeSort.Count - 1; i++) + { + var node = Nodes.ActivedNodes.FirstOrDefault(n => n.Id == NodeSort[i].Id); + node?.UpdatePosition(node.X, NodeSort[0].Y + offset * i); + } + } + } + + public void ZoneUpdateShape(double x, double y) + { + if (Zones.ZoneActived is not null) + { + if (EditorBackup.Count == 0 || EditorBackup.Last().Id != Zones.ZoneActived.Id || EditorBackup.Count > 0 && EditorBackup.Last().Obj is ZoneShapeBackup zonebackup && zonebackup.NodeChange != Zones.ZoneActived.ActiveNode) + { + EditorBackup.Add(new() + { + Id = Zones.ZoneActived.Id, + Type = MapEditorBackupType.Zone, + Obj = new ZoneShapeBackup() + { + NodeChange = Zones.ZoneActived.ActiveNode, + X1 = Zones.ZoneActived.X1, + Y1 = Zones.ZoneActived.Y1, + X2 = Zones.ZoneActived.X2, + Y2 = Zones.ZoneActived.Y2, + X3 = Zones.ZoneActived.X3, + Y3 = Zones.ZoneActived.Y3, + X4 = Zones.ZoneActived.X4, + Y4 = Zones.ZoneActived.Y4, + } + }); + NodesUndoableChanged.InvokeAsync(true); + } + if (Zones.ZoneActived.ActiveNode > 0) Zones.ZoneActived.UpdateControlNode(Zones.ZoneActived.ActiveNode, x, y); + } + } + + public void EdgesPositionMove(double x, double y) + { + if (EditorBackup.Count == 0) + { + List BackupEdge = []; + foreach (var edge in Edges.ActivedEdges) + { + BackupEdge.Add(new() + { + Id = edge.Id, + TrajectoryDegree = edge.TrajectoryDegree, + StartX = edge.X1, + StartY = edge.Y1, + EndX = edge.X2, + EndY = edge.Y2, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + }); + } + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveEdge, + Obj = BackupEdge, + }); + NodesUndoableChanged.InvokeAsync(true); + } + Edges.UpdateMoveEdge(x, y); + } + + public void NodesPositionMove(double x, double y) + { + if (EditorBackup.Count == 0) + { + List ActiveNode = []; + foreach (var node in Nodes.ActivedNodes) + { + ActiveNode.Add(new() + { + Id = node.Id, + X = x, + Y = y, + }); + } + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MoveNode, + Obj = ActiveNode, + }); + NodesUndoableChanged.InvokeAsync(true); + } + Nodes.UpdateMoveNode(x, y); + } + + public void NodePositionMove(double x, double y) + { + if (Nodes.SelectedNode is not null) + { + if (EditorBackup.Count == 0 || EditorBackup.Last().Id != Nodes.SelectedNode.Id) + { + EditorBackup.Add(new() + { + Id = Nodes.SelectedNode.Id, + Type = MapEditorBackupType.Node, + Obj = new PositionBackup() + { + Id = Nodes.SelectedNode.Id, + X = Nodes.SelectedNode.X, + Y = Nodes.SelectedNode.Y, + } + }); + NodesUndoableChanged.InvokeAsync(true); + } + + Nodes.SelectedNode.UpdatePosition(x, y); + } + else if (Edges.ActivedEdges.Count == 1) + { + if (Edges.ActivedEdges[0].ActivedControlPoint1) + { + if (EditorBackup.Count == 0 || EditorBackup.Last().Id != Edges.ActivedEdges[0].Id || EditorBackup.Last().Id == Edges.ActivedEdges[0].Id && EditorBackup.Last().Type != MapEditorBackupType.ControlPoint1Edge) + { + EditorBackup.Add(new() + { + Id = Edges.ActivedEdges[0].Id, + Type = MapEditorBackupType.ControlPoint1Edge, + Obj = new PositionBackup() + { + Id = Edges.ActivedEdges[0].Id, + X = Edges.ActivedEdges[0].ControlPoint1X, + Y = Edges.ActivedEdges[0].ControlPoint1Y, + } + }); + NodesUndoableChanged.InvokeAsync(true); + } + Edges.ActivedEdges[0].UpdateControlPoint1(x, y); + } + else if (Edges.ActivedEdges[0].ActivedControlPoint2) + { + if (EditorBackup.Count == 0 || EditorBackup.Last().Id != Edges.ActivedEdges[0].Id || EditorBackup.Last().Id == Edges.ActivedEdges[0].Id && EditorBackup.Last().Type != MapEditorBackupType.ControlPoint2Edge) + { + EditorBackup.Add(new() + { + Id = Edges.ActivedEdges[0].Id, + Type = MapEditorBackupType.ControlPoint2Edge, + Obj = new PositionBackup() + { + Id = Edges.ActivedEdges[0].Id, + X = Edges.ActivedEdges[0].ControlPoint2X, + Y = Edges.ActivedEdges[0].ControlPoint2Y, + } + }); + NodesUndoableChanged.InvokeAsync(true); + } + Edges.ActivedEdges[0].UpdateControlPoint2(x, y); + } + } + } + + public async Task SplitNode() + { + if (EditorBackup.Count > 0) + { + if (!await SaveChanged()) return; + } + + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return; } + + if (Nodes.ActivedNodes.Count > 1) { Snackbar.Add("Vui lòng chỉ chọn 1 Node để tách", Severity.Warning); return; } + if (Nodes.ActivedNodes.Count == 0) { Snackbar.Add("Vui lòng chọn 1 Node để tách", Severity.Warning); return; } + + var nodeSplit = Nodes.ActivedNodes[0]; + + var edges = Edges.Where(edge => edge.StartNode.Id == nodeSplit.Id || edge.EndNode.Id == nodeSplit.Id).ToList(); + if (edges.Count <= 1) { Snackbar.Add("Không thể tách node đơn", Severity.Warning); return; } + + Dictionary EdgeSplit = []; + for (int i = 1; i < edges.Count; i++) + { + var newNode = new NodeDto() + { + Id = Guid.NewGuid(), + Name = string.Empty, + X = nodeSplit.X, + Y = nodeSplit.Y, + Theta = nodeSplit.Theta, + MapId = nodeSplit.MapId, + AllowedDeviationXy = nodeSplit.AllowedDeviationXy, + AllowedDeviationTheta = nodeSplit.AllowedDeviationTheta, + Actions = nodeSplit.Actions, + }; + Nodes.Add(newNode); + edges[i].UpdateNode(nodeSplit.Id, Nodes[newNode.Id]); + EdgeSplit.Add(edges[i].Id, newNode); + } + Nodes.UnActivedNode(); + EditorBackup.Add(new() + { + Type = MapEditorBackupType.SplitNode, + Id = nodeSplit.Id, + Obj = new SplitNodeBackup() + { + NodeId = nodeSplit.Id, + EdgeSplit = EdgeSplit, + }, + }); + await NodesUndoableChanged.InvokeAsync(true); + Snackbar.Add("Tách Node thành công", Severity.Success); + } + + public async Task MergeNode() + { + if (EditorBackup.Count > 0) + { + if (!await SaveChanged()) return; + } + + if (MapData is null) { Snackbar.Add("Dữ liệu bản đồ rỗng", Severity.Warning); return; } + if (MapData.Active) { Snackbar.Add("Không thể tạo edge do bản đồ đang được Active", Severity.Warning); return; } + + if (Nodes.ActivedNodes.Count <= 1) { Snackbar.Add("Vui lòng chọn nhiều hơn 1 Node để gộp", Severity.Warning); return; } + if (Edges.ActivedEdges.Count > 0) { Snackbar.Add("Vui lòng không chọn Edge", Severity.Warning); return; } + + MergeNodeBackup MergeNodeBackup = new() + { + NodeId = Nodes.ActivedNodes[0].Id, + }; + List activedNodes = [.. Nodes.ActivedNodes]; + Nodes.UnActivedNode(); + for (int i = 1; i < activedNodes.Count; i++) + { + var edges = Edges.Where(e => e.StartNode.Id == activedNodes[i].Id || e.EndNode.Id == activedNodes[i].Id).ToList(); + if (edges.Count > 0) + { + foreach (var edge in edges) + { + edge.UpdateNode(activedNodes[i].Id, Nodes[activedNodes[0].Id]); + MergeNodeBackup.EdgesMerge.Add((edge.Id, activedNodes[i])); + Nodes.Remove(activedNodes[i]); + } + } + } + EditorBackup.Add(new() + { + Type = MapEditorBackupType.MergeNode, + Id = activedNodes[0].Id, + Obj = MergeNodeBackup, + }); + await NodesUndoableChanged.InvokeAsync(true); + Snackbar.Add("Gộp Node thành công", Severity.Success); + } + + public async Task ScanerActive(double x1, double y1, double x2, double y2) + { + List activeEdges = []; + List activeNodes = []; + foreach (var edge in Edges) + { + if (MapEditorHelper.NodeInScanZone(edge.StartNode.X, edge.StartNode.Y, x1, y1, x2, y2) && MapEditorHelper.NodeInScanZone(edge.EndNode.X, edge.EndNode.Y, x1, y1, x2, y2)) + { + activeEdges.Add(edge); + if (!activeNodes.Any(n => n.Id == edge.StartNode.Id)) activeNodes.Add(edge.StartNode); + if (!activeNodes.Any(n => n.Id == edge.EndNode.Id)) activeNodes.Add(edge.EndNode); + } + } + foreach (var node in Nodes) + { + if (!activeNodes.Any(n => n.Id == node.Id) && MapEditorHelper.NodeInScanZone(node.X, node.Y, x1, y1, x2, y2)) activeNodes.Add(node); + } + + Edges.ActivedEdge(activeEdges); + Nodes.ActivedNode(activeNodes); + + await MultiselectedNodeChanged.InvokeAsync(activeNodes.Count > 0); + await MultiselectedEdgeChanged.InvokeAsync(activeEdges.Count > 0); + } + + public async Task CheckMap() + { + await MapIsChecking.InvokeAsync(true); + + var mapSetting = await Http.GetFromJsonAsync>($"api/MapsSetting/{MapData.Id}"); + + if (mapSetting is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!mapSetting.IsSuccess) { Snackbar.Add(mapSetting.Message ?? "Lấy dữ liệu bản đồ không thành công", Severity.Error); return; } + else if (mapSetting.Data == null) { Snackbar.Add("Lỗi dữ liệu trả về", Severity.Error); return; } + + foreach (var edge in Edges) + { + var caculateEdge = new EdgeCaculatorModel() + { + MapId = edge.MapId, + X1 = edge.X1, + Y1 = edge.Y1, + X2 = edge.X2, + Y2 = edge.Y2, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + TrajectoryDegree = edge.TrajectoryDegree, + }; + if (MapEditorHelper.GetEdgeLength(caculateEdge) < mapSetting.Data.EdgeMinLength) edge.SetError(true); + else edge.SetError(false); + } + + List ErrorZones = []; + foreach (var zone in Zones) + { + if (MapEditorHelper.CalculateQuadrilateralArea(zone.X1, zone.Y1, zone.X2, zone.Y2, zone.X3, zone.Y3, zone.X4, zone.Y4) < mapSetting.Data.ZoneMinSquare) + { + ErrorZones.Add(zone); + } + } + + List ErrorNodes = []; + foreach (var node in Nodes) + { + if (ErrorNodes.Any(e => e.Id == node.Id)) continue; + foreach (var checkNode in Nodes) + { + if (checkNode.Id == node.Id) continue; + var distance = Math.Sqrt(Math.Pow(node.X - checkNode.X, 2) + Math.Pow(node.Y - checkNode.Y, 2)); + + if (distance < mapSetting.Data.EdgeMinLength) + { + if (!Edges.Any(e => e.StartNode.Id == node.Id && e.EndNode.Id == checkNode.Id || e.EndNode.Id == node.Id && e.StartNode.Id == checkNode.Id)) + { + ErrorNodes.Add(node); + node.SetError(true); + break; + } + } + } + if (!ErrorNodes.Any(e => e.Id == node.Id)) node.SetError(false); + } + + await MapIsChecking.InvokeAsync(false); + Snackbar.Add("Kiểm tra xong", Severity.Success); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.css b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.css new file mode 100644 index 0000000..ebedac5 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapContainer.razor.css @@ -0,0 +1,28 @@ +.map-viewer-container { + background-color: #bfbfbf; + width: 100%; + height: 100%; + cursor: not-allowed; + border-top: solid 2px #808080; + overflow: hidden; + position: relative; +} + + .map-viewer-container > div { + width: fit-content; + height: fit-content; + position: absolute; + cursor: default; + display: flex; + overflow: hidden; + } + +.map-image { + user-select: none; + image-rendering: pixelated; + transform: scale(1, -1); +} + +.map-editor { + transform: scale(1, -1); +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapCopy.razor b/RobotNet.WebApp/Maps/Components/Editor/MapCopy.razor new file mode 100644 index 0000000..cb9f9d1 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapCopy.razor @@ -0,0 +1,121 @@ +@implements IDisposable + +@using RobotNet.WebApp.Maps.Components.Editor.Edge +@using RobotNet.WebApp.Maps.Components.Editor.Node + + + + +@code { + public MapEdgeModel EdgeModels { get; set; } = []; + public MapNodeModel NodeModels { get; set; } = []; + public bool isMoving = false; + + private bool isShowing = false; + private (Guid Id, double X, double Y) StartPosition = new(); + + public void OnShow(bool isShow, List edgeModels) + { + isShowing = isShow; + if (isShow && edgeModels.Count > 0) + { + isMoving = false; + List nodeDtos = []; + foreach (var edge in edgeModels) + { + if (!nodeDtos.Any(n => n.Id == edge.StartNode.Id)) nodeDtos.Add(new() + { + Id = edge.StartNode.Id, + MapId = edge.StartNode.MapId, + Name = edge.StartNode.Name, + X = edge.StartNode.X, + Y = edge.StartNode.Y, + Theta = edge.StartNode.Theta, + AllowedDeviationXy = edge.StartNode.AllowedDeviationXy, + AllowedDeviationTheta = edge.StartNode.AllowedDeviationTheta, + Actions = edge.StartNode.Actions, + }); + if (!nodeDtos.Any(n => n.Id == edge.EndNode.Id)) nodeDtos.Add(new() + { + Id = edge.EndNode.Id, + MapId = edge.EndNode.MapId, + Name = edge.EndNode.Name, + X = edge.EndNode.X, + Y = edge.EndNode.Y, + Theta = edge.EndNode.Theta, + AllowedDeviationXy = edge.EndNode.AllowedDeviationXy, + AllowedDeviationTheta = edge.EndNode.AllowedDeviationTheta, + Actions = edge.EndNode.Actions, + }); + } + + NodeModels.ReplaceAll(nodeDtos); + List edges = edgeModels.Select(edge => new EdgeModel(new() + { + Id = edge.Id, + MapId = edge.MapId, + DirectionAllowed = edge.DirectionAllowed, + TrajectoryDegree = edge.TrajectoryDegree, + Actions = edge.Actions, + AllowedDeviationTheta = edge.AllowedDeviationTheta, + AllowedDeviationXy = edge.AllowedDeviationXy, + RotationAllowed = edge.RotationAllowed, + MaxHeight = edge.MaxHeight, + MinHeight = edge.MinHeight, + MaxRotationSpeed = edge.MaxRotationSpeed, + MaxSpeed = edge.MaxRotationSpeed, + ControlPoint1X = edge.ControlPoint1X, + ControlPoint1Y = edge.ControlPoint1Y, + ControlPoint2X = edge.ControlPoint2X, + ControlPoint2Y = edge.ControlPoint2Y, + }, + NodeModels[edge.StartNode.Id], + NodeModels[edge.EndNode.Id] + )).ToList(); + + EdgeModels.ReplaceAll(edges); + EdgeModels.ActivedEdge(edges); + if (NodeModels.Count() > 0) + { + StartPosition.Id = NodeModels[0].Id; + StartPosition.X = NodeModels[0].X; + StartPosition.Y = NodeModels[0].Y; + } + } + else + { + isMoving = false; + NodeModels.ReplaceAll([]); + EdgeModels.ReplaceAll([]); + } + + StateHasChanged(); + } + + public void UpdateMove(double x, double y) + { + if (!isShowing) return; + if (NodeModels.Count() > 0) + { + var nodeChange = NodeModels[StartPosition.Id]; + if (nodeChange is not null) + { + var distance = Math.Sqrt(Math.Pow(StartPosition.X - nodeChange.X, 2) + Math.Pow(StartPosition.Y - nodeChange.Y, 2)); + if (distance > 0.25) isMoving = true; + else isMoving = false; + } + + EdgeModels.UpdateMoveEdge(x, y); + } + } + + public void SetStartMovePosition(double x, double y) + { + EdgeModels.SetStartMovePosition(x, y); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor b/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor new file mode 100644 index 0000000..965e77f --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor @@ -0,0 +1,37 @@ + + @for (int x = startX; x <= endX; x += 1) + { + + } + @for (int y = startY; y <= endY; y += 1) + { + + } + + +@code { + [Parameter, EditorRequired] + public bool Show { get; set; } + + private double originX; + private double originY; + private int startX; + private int startY; + private int endX; + private int endY; + private double width; + private double height; + + public void Resize(double mapOriginX, double mapOriginY, double mapheight, double mapwidth) + { + width = mapwidth; + height = mapheight; + originX = mapOriginX; + originY = -height - mapOriginY; + startX = (int)Math.Ceiling(originX); + startY = (int)Math.Ceiling(originY); + endX = (int)Math.Floor(width + mapOriginX); + endY = (int)Math.Floor(-mapOriginY); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor.css b/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor.css new file mode 100644 index 0000000..ca81f84 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapGrid.razor.css @@ -0,0 +1,6 @@ +line { + stroke: gray; + stroke-width: 0.04px; + stroke-opacity: 1; + stroke-dasharray: 0.05 0.1; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor b/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor new file mode 100644 index 0000000..c4142f2 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor @@ -0,0 +1,20 @@ +
+
+ @X +
+
+ @Y +
+
+ +@code { + private double X; + private double Y; + + public void Update(double x, double y) + { + X = Math.Round(x, 3); + Y = Math.Round(y, 3); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor.css b/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor.css new file mode 100644 index 0000000..d40e5cf --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapMousePosition.razor.css @@ -0,0 +1,14 @@ +div { + width:fit-content; + height: fit-content; +} +.map-editor-info { + color: #00cc66; + font-size: 15px; + position: absolute; + top: 5px; + left: 5px; + font-weight: bold; + background-color:white; + border-radius:4px +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor b/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor new file mode 100644 index 0000000..b0b08e8 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor @@ -0,0 +1,40 @@ +@inject IJSRuntime JSRuntime + + + +@code { + private ElementReference Ref; + + public double X1 { get; private set; } + public double Y1 { get; private set; } + public double X2 { get; private set; } + public double Y2 { get; private set; } + + public async Task CreateAsync(double x, double y) + { + X1 = x; + Y1 = y; + X2 = x; + Y2 = y; + + await CalculatorZone(); + await Visible(); + } + + public async Task Update(double x, double y) + { + X2 = x; + Y2 = y; + await CalculatorZone(); + } + + private async Task CalculatorZone() + { + var points = $"{X1},{Y1} {X1},{Y2} {X2},{Y2} {X2},{Y1}"; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", points); + } + + + public async Task Visible() => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "visible"); + public async Task Hidden() => await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "hidden"); +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor.css b/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor.css new file mode 100644 index 0000000..5edc74a --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapScaner.razor.css @@ -0,0 +1,5 @@ +polygon { + fill: #c2c2a3; + stroke: none; + fill-opacity: 0.5; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor b/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor new file mode 100644 index 0000000..2ff5640 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + +@code { + public static string GetMakerMid(DirectionAllowed direction) + { + return direction switch + { + DirectionAllowed.Both => $"url(#edge-two-way)", + DirectionAllowed.Forward => $"url(#edge-forward)", + DirectionAllowed.Backward => $"url(#edge-backward)", + DirectionAllowed.None => $"url(#edge-none)", + _ => "", + }; + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor.css b/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor.css new file mode 100644 index 0000000..1a2be22 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/MapSvgDefs.razor.css @@ -0,0 +1,5 @@ +polyline { + stroke: #DD22FF; + stroke-width: 0.05px; + fill: none; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor b/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor new file mode 100644 index 0000000..f63a8bd --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor @@ -0,0 +1,489 @@ +@using System.Text.Json +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.WebApp.Maps.Components.Editor.Element + +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory +@inject IDialogService Dialog + +@foreach (var node in Models) +{ + +} + +@foreach (var element in Elements) +{ + +} + + + + + Update node @UpdateModel.Id + + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + @foreach (var action in Actions) + { + @action.Name + } + + +
+
+ @foreach (var actionId in UpdateModel.Actions) + { +
+ + @(Actions.FirstOrDefault(ac => ac.Id == actionId)?.Name) + +
+ } +
+
+
+
+ + Cancel + Update + +
+ + + + + Create Element Node @ElementCreateModel.NodeId + + + +
+ +
+ + +
+ + @foreach (var model in ElementModels) + { + @model.Name + } + +
+
+ + Cancel + Create + +
+ + + + + Update Element @ElementUpdateModel.Name + + + +
+
+ + + + @* *@ +
+
+
+ @foreach (var property in ElementProperties) + { + @if (property.Type == typeof(int).ToString()) + { + if (int.TryParse(property.DefaultValue, out int value)) + { + + } + } + else if (property.Type == typeof(double).ToString()) + { + if (double.TryParse(property.DefaultValue, out double value)) + { + + } + } + else if (property.Type == typeof(bool).ToString()) + { + if (bool.TryParse(property.DefaultValue, out bool value)) + { + + } + } + else if (property.Type == typeof(string).ToString()) + { + + } + } +
+ Properties +
+
+
+ + Delete + Cancel + Update + +
+ +@code { + [Parameter, EditorRequired] + public MapNodeModel Models { get; set; } = null!; + + [Parameter] + public MapElementModel Elements { get; set; } = []; + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + private NodeUpdateModel UpdateModel = new(); + private bool updateNodeVisible; + + private List Actions = []; + private ActionDto? ActionSelected = null; + + public List ElementModels { get; set; } = []; + private bool createElementVisible; + private ElementCreateModel ElementCreateModel = new(); + private ElementModelDto? ElementModelSelected = null; + private NodeModel? NodeModelDoubleClick = null; + + private bool updateElementVisble; + private ElementUpdateModel ElementUpdateModel = new(); + private List ElementProperties = []; + private HttpClient Http = default!; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + Models.Changed += StateHasChanged; + Elements.Changed += StateHasChanged; + } + + private void NodeClickChanged(NodeModel? node) => Models.SelectedNode = node; + + private async Task LoadActionAsync(Guid mapId) + { + var result = await Http.GetFromJsonAsync>($"api/Actions/{mapId}"); + + if (result is not null && result.Any()) + { + Actions.Clear(); + Actions.AddRange(result); + } + if (Actions.Any()) ActionSelected = Actions.First(); + + StateHasChanged(); + } + + private async Task LoadElementModels(Guid mapId) + { + ElementModels.Clear(); + + var elModels = await Http.GetFromJsonAsync>>($"api/ElementModels/map/{mapId}"); + if (elModels is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!elModels.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elModels.Message}", Severity.Error); + else if(elModels.Data is null || !elModels.Data.Any()) + { + Snackbar.Add("Không có Element model nào trong bản đồ này", Severity.Warning); + ElementModelSelected = null; + } + else + { + ElementModels.AddRange(elModels.Data.OrderBy(el => el.Name)); + if (ElementModels.Count > 0) + { + ElementModelSelected = ElementModelSelected is null ? ElementModels.First() : ElementModels.FirstOrDefault(em => em.Id == ElementModelSelected.Id) ?? ElementModels.First(); + } + } + StateHasChanged(); + } + + private void AddAction() + { + if (ActionSelected is null) return; + if (UpdateModel.Actions.Any(action => action == ActionSelected.Id)) + { + Snackbar.Add("Action đã tồn tại", Severity.Warning); + return; + } + + UpdateModel.Actions = [.. UpdateModel.Actions, ActionSelected.Id]; + StateHasChanged(); + } + + private void DeleteAction(Guid actionId) + { + UpdateModel.Actions = UpdateModel.Actions.Where(a => actionId != a).ToArray(); + StateHasChanged(); + } + + private async Task OnNodeDoubleClick(NodeModel model) + { + if (model == null || (EditorState != EditorState.NavigationEdit && EditorState != EditorState.View)) return; + + NodeModelDoubleClick = model; + if (EditorState == EditorState.NavigationEdit) + { + await LoadActionAsync(model.MapId); + + UpdateModel.Id = model.Id; + UpdateModel.Name = model.Name; + UpdateModel.X = model.X; + UpdateModel.Y = model.Y; + UpdateModel.Theta = model.Theta; + UpdateModel.AllowedDeviationXy = model.AllowedDeviationXy; + UpdateModel.AllowedDeviationTheta = model.AllowedDeviationTheta; + UpdateModel.Actions = []; + var actions = JsonSerializer.Deserialize(model.Actions ?? ""); + if (actions is not null && actions.Length > 0) + { + UpdateModel.Actions = [.. actions]; + } + + updateNodeVisible = true; + } + else if (EditorState == EditorState.View) + { + await LoadElementModels(model.MapId); + + ElementCreateModel.Name = ""; + ElementCreateModel.MapId = model.MapId; + ElementCreateModel.NodeId = model.Id; + + createElementVisible = true; + } + StateHasChanged(); + } + + private async Task UpdateNode() + { + var selectedModel = Models.FirstOrDefault(e => e.Id == UpdateModel.Id); + + if (selectedModel == null) + { + updateNodeVisible = false; + StateHasChanged(); + return; + } + + var result = await (await Http.PutAsJsonAsync($"api/Nodes", UpdateModel)).Content.ReadFromJsonAsync(); + if (result is null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + selectedModel.UpdateData(UpdateModel); + updateNodeVisible = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + + private async Task CreateElement() + { + try + { + if (NodeModelDoubleClick is null) return; + if (ElementModelSelected is null) + { + Snackbar.Add("Hãy chọn Element model!", Severity.Warning); + return; + } + if (string.IsNullOrEmpty(ElementCreateModel.Name)) + { + Snackbar.Add("Name không được để trống.", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(ElementCreateModel.Name); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + ElementCreateModel.ModelId = ElementModelSelected.Id; + + var create = await (await Http.PostAsJsonAsync("api/Elements", ElementCreateModel)).Content.ReadFromJsonAsync>(); + if (create is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!create.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {create.Message}", Severity.Error); + else if (create.Data is null) Snackbar.Add("Tạo Element không thành công", Severity.Error); + else + { + + Elements.Add(new(create.Data, NodeModelDoubleClick)); + createElementVisible = false; + Snackbar.Add("Tạo Element thành công!", Severity.Success); + } + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private void ElementDoubleClick(ElementModel model) + { + ElementUpdateModel.Id = model.Id; + ElementUpdateModel.Name = model.Name; + ElementUpdateModel.IsOpen = model.IsOpen; + ElementUpdateModel.OffsetX = model.OffsetX; + ElementUpdateModel.OffsetY = model.OffsetY; + ElementUpdateModel.Content = model.Content; + + if (model is not null && !string.IsNullOrEmpty(model.Content)) + { + var properties = JsonSerializer.Deserialize>(model.Content, JsonOptionExtends.Read); + if (properties is not null) ElementProperties = [.. properties]; + } + + updateElementVisble = true; + StateHasChanged(); + } + + private async Task Delete() + { + var parameters = new DialogParameters + { + { x => x.Content, $"Bạn có chắc chắn muốn xóa element {ElementUpdateModel.Name} đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Element", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var response = await Http.DeleteFromJsonAsync($"api/Elements/{ElementUpdateModel.Id}"); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var elementData = Elements.FirstOrDefault(m => m.Id == ElementUpdateModel.Id); + if (elementData is not null) + { + Elements.Remove(elementData); + Snackbar.Add($"Xóa element thành công.", Severity.Success); + } + else Snackbar.Add($"Element không tồn tại.", Severity.Warning); + } + + updateElementVisble = false; + StateHasChanged(); + } + } + + private void DefaultIntValueChanged(Guid id, int value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultDoubleValueChanged(Guid id, double value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultBooleanValueChanged(Guid id, bool value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private async Task UpdateElement() + { + var selectedElement = Elements.FirstOrDefault(e => e.Id == ElementUpdateModel.Id); + if (selectedElement is null) return; + + var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel() + { + Id = ElementUpdateModel.Id, + Name = ElementUpdateModel.Name, + IsOpen = ElementUpdateModel.IsOpen, + OffsetX = ElementUpdateModel.OffsetX, + OffsetY = ElementUpdateModel.OffsetY, + Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write), + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error); + else + { + selectedElement.UpdateElement(result.Data); + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + + updateElementVisble = false; + StateHasChanged(); + } + + public void Dispose() + { + Models.Changed -= StateHasChanged; + Elements.Changed -= StateHasChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor.css new file mode 100644 index 0000000..263e990 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Node/MapNode.razor.css @@ -0,0 +1,44 @@ +.paper-action { + padding: 5px; + border: 1px solid silver; + border-radius: 10px; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + max-width: 290px; + height: 100%; + max-height: 277px; +} + +.paper-property-container { + position: relative; + display: inline-block; + width: 50%; + height: 193px; +} + +.paper-title { + font-size: 12px; + padding: 0 5px; + position: absolute; + background-color: var(--mud-palette-surface); + top: 0px; + left: 15px; +} + +.paper-property { + padding: 10px; + border-radius: var(--mud-default-borderradius); + border: 1px solid var(--mud-palette-lines-inputs); + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + overflow-x: hidden; + overflow-y: auto; + margin: 8px; +} \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor b/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor new file mode 100644 index 0000000..f94c228 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor @@ -0,0 +1,102 @@ +@implements IDisposable +@inject IJSRuntime JSRuntime + + + +@Model?.Name + +@code { + [Parameter, EditorRequired] + public NodeModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + private ElementReference circleRef; + private ElementReference textRef; + private ElementReference circleErrorRef; + private DotNetObjectReference DotNetObj = null!; + private bool IsError = false; + + private bool IsSetting => EditorState == EditorState.NavigationEdit || EditorState == EditorState.CreateStraighEdge || EditorState == EditorState.CreateCurveEdge || + EditorState == EditorState.CreateDoubleCurveEdge || EditorState == EditorState.View; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, circleRef, nameof(OnMouseDown), false); + await UpdatePosition(Model.X, Model.Y); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", circleErrorRef, "visibility", "hidden"); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateNode = false; + if (parameters.TryGetValue(nameof(Model), out NodeModel? model) && ((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) + { + Model.PositionChanged -= Model_PositionChanged; + Model.ActivedChanged -= ActivedChanged; + Model.ErrorChanged -= ErrorChanged; + } + + if (model != null) + { + updateNode = true; + model.PositionChanged += Model_PositionChanged; + model.ActivedChanged += ActivedChanged; + model.ErrorChanged += ErrorChanged; + } + } + await base.SetParametersAsync(parameters); + if (updateNode && model != null) await UpdatePosition(model.X, model.Y); + } + + private async void Model_PositionChanged() => await UpdatePosition(Model.X, Model.Y); + + [JSInvokable] + public async Task OnMouseDown(int button, bool altKey, bool ctrlKey, bool shiftKey) => await OnClick.InvokeAsync(Model); + + private async Task UpdatePosition(double x, double y) + { + await JSRuntime.InvokeVoidAsync("SetNodePosition", circleRef, textRef, x, y); + if (IsError) await JSRuntime.InvokeVoidAsync("SetNodePosition", circleErrorRef, textRef, x, y); + } + + private async void ActivedChanged(bool state) => await JSRuntime.InvokeVoidAsync(state ? "AddSelected" : "RemoveSelected", circleRef); + + private async void ErrorChanged(bool state) + { + IsError = state; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", circleErrorRef, "visibility", state ? "visible" : "hidden"); + if (IsError) await JSRuntime.InvokeVoidAsync("SetNodePosition", circleErrorRef, textRef, Model.X, Model.Y); + } + + public void Dispose() + { + if (Model != null) + { + Model.PositionChanged -= Model_PositionChanged; + Model.ActivedChanged -= ActivedChanged; + Model.ErrorChanged -= ErrorChanged; + } + if (DotNetObj != null) DotNetObj.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor.css new file mode 100644 index 0000000..b088c2c --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Node/Node.razor.css @@ -0,0 +1,41 @@ +circle { + cursor: default; + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} + + circle.setting:hover { + stroke-width: 0.02px; + cursor: pointer; + } + + circle.setting:active { + stroke-width: 0.02px; + stroke: green; + } + + circle.active { + stroke-width: 0.02px; + stroke: green; + } + + circle:hover + text.setting { + transition-delay: 0.1s; + font-weight: bold; + } + + circle + text { + transition-delay: 0.3s; + font-size: 0.01em; + font-weight: normal; + } + +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + user-select: none; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor b/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor new file mode 100644 index 0000000..b684e37 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor.css b/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor.css new file mode 100644 index 0000000..2a68c05 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/OriginVector.razor.css @@ -0,0 +1,4 @@ +line.origin { + stroke-width: var(--origin-vector-stroke-width); + fill: none; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/MapZone.razor b/RobotNet.WebApp/Maps/Components/Editor/Zone/MapZone.razor new file mode 100644 index 0000000..7c5f3d8 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/MapZone.razor @@ -0,0 +1,156 @@ +@implements IDisposable + +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + +@foreach (var zone in Models) +{ + +} + + + + + Update zone @UpdateModel.Id + + + + + + + + + + + + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + Cancel + Update + +
+ +@code { + [Parameter, EditorRequired] + public MapZoneModel Models { get; set; } = null!; + + [Parameter] + public bool IsShow { get; set; } + + [Parameter] + public bool IsSetting { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + [Parameter] + public EventCallback<(ZoneModel, bool)> ActivedZoneChanged { get; set; } + + private ZoneUpdateModel UpdateModel = new(); + private bool UpdateZoneVisiable = false; + private HttpClient Http = default!; + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + Models.Changed += StateHasChanged; + Models.ActivedZoneChanged += OnActivedZoneChanged; + } + + private void OnActivedZoneChanged(ZoneModel model, bool state) + { + _ = ActivedZoneChanged.InvokeAsync((model, state)); + } + + private void ZoneDoubleClick(ZoneModel model) + { + if (model == null || EditorState != EditorState.SettingZone) return; + + UpdateModel.Id = model.Id; + UpdateModel.Type = model.Type; + UpdateModel.Name = model.Name; + UpdateModel.X1 = model.X1; + UpdateModel.Y1 = model.Y1; + UpdateModel.X2 = model.X2; + UpdateModel.Y2 = model.Y2; + UpdateModel.X3 = model.X3; + UpdateModel.Y3 = model.Y3; + UpdateModel.X4 = model.X4; + UpdateModel.Y4 = model.Y4; + + UpdateZoneVisiable = true; + StateHasChanged(); + } + + private void CancelUpdateZone() + { + UpdateZoneVisiable = false; + StateHasChanged(); + } + + private async Task UpdateZone() + { + var selectedModel = Models.FirstOrDefault(e => e.Id == UpdateModel.Id); + + if (selectedModel == null) + { + UpdateZoneVisiable = false; + StateHasChanged(); + return; + } + + var result = await (await Http.PutAsJsonAsync($"api/Zones", UpdateModel)).Content.ReadFromJsonAsync(); + if (result is null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + selectedModel.UpdateData(UpdateModel); + UpdateZoneVisiable = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + + public void Dispose() + { + Models.Changed -= StateHasChanged; + Models.ActivedZoneChanged -= OnActivedZoneChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor b/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor new file mode 100644 index 0000000..ac13763 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor @@ -0,0 +1,95 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + +@code { + [Parameter, EditorRequired] + public ZoneModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [Parameter] + public bool IsSetting { get; set; } + + [Parameter] + public bool IsShow { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + private ElementReference Ref; + private DotNetObjectReference DotNetObj = null!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("AddEventListener", DotNetObj, Ref, "click", nameof(Click), false); + await UpdateData(); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool UpdateZone = false; + if (parameters.TryGetValue(nameof(Model), out ZoneModel? model)) + { + if (((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) + { + Model.ControlNodePosittionChanged -= UpdateData; + Model.TypeChanged -= StateHasChanged; + Model.ActiveChanged -= ActiveChanged; + } + + if (model != null) + { + model.ControlNodePosittionChanged += UpdateData; + model.TypeChanged += StateHasChanged; + model.ActiveChanged += ActiveChanged; + + UpdateZone = true; + } + } + } + + await base.SetParametersAsync(parameters); + if (UpdateZone) await UpdateData(); + } + + private async Task OnDoubleClick() + { + await DoubleClick.InvokeAsync(Model); + } + + [JSInvokable] + public void Click() + { + Model.Active(); + } + + private async void ActiveChanged(ZoneModel zone, bool state) => await JSRuntime.InvokeVoidAsync(state ? "AddSelected" : "RemoveSelected", Ref); + + private async Task UpdateData() + { + string data = $"{Model.X1},{Model.Y1} {Model.X2},{Model.Y2} {Model.X3},{Model.Y3} {Model.X4},{Model.Y4}"; + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", data); + } + + public void Dispose() + { + if (Model != null) + { + Model.ControlNodePosittionChanged -= UpdateData; + Model.ActiveChanged -= ActiveChanged; + Model.TypeChanged -= StateHasChanged; + } + if (DotNetObj != null) DotNetObj.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor.css new file mode 100644 index 0000000..4c27723 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/Zone.razor.css @@ -0,0 +1,29 @@ +polygon { + stroke: none; + fill-opacity: 0.5; + stroke-linejoin: round; + stroke-linecap: butt; + position: relative; + z-index: 1; +} + + polygon.setting:hover { + stroke-width: 0.02px; + stroke: red; + cursor: pointer; + } + + polygon.setting:active { + fill-opacity: 0.7; + stroke-width: 0.02px; + stroke: green; + cursor: pointer; + } + + polygon.setting.active { + fill-opacity: 0.7; + stroke-width: 0.02px; + stroke: green; + cursor: pointer; + z-index: 2; + } diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor new file mode 100644 index 0000000..da9c08d --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor @@ -0,0 +1,137 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + + + 1 + + 2 + + 3 + + 4 + + +@code { + private ElementReference Node1Ref; + private ElementReference Node2Ref; + private ElementReference Node3Ref; + private ElementReference Node4Ref; + private DotNetObjectReference DotNetObj = null!; + + private double X1; + private double Y1; + private double X2; + private double Y2; + private double X3; + private double Y3; + private double X4; + private double Y4; + private ZoneModel? Model; + private string Visibility = "hidden"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, Node1Ref, nameof(OnMouseDownNode1), false); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, Node2Ref, nameof(OnMouseDownNode2), false); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, Node3Ref, nameof(OnMouseDownNode3), false); + await JSRuntime.InvokeVoidAsync("AddMouseDownEventListener", DotNetObj, Node4Ref, nameof(OnMouseDownNode4), false); + } + + public void SetControl(ZoneModel? model) + { + if (Model != null) + { + Model.ControlNodePosittionChanged -= CaculateZoneAsync; + } + + Model = model; + if (Model != null) + { + Visibility = "visible"; + CaculateZone(); + + Model.ControlNodePosittionChanged += CaculateZoneAsync; + Visibility = "visible"; + } + else + { + Visibility = "hidden"; + } + StateHasChanged(); + } + + private Task CaculateZoneAsync() + { + CaculateZone(); + return Task.CompletedTask; + } + + private void CaculateZone() + { + if (Model == null) return; + + X1 = Model.X1; + Y1 = Model.Y1; + X2 = Model.X2; + Y2 = Model.Y2; + X3 = Model.X3; + Y3 = Model.Y3; + X4 = Model.X4; + Y4 = Model.Y4; + StateHasChanged(); + } + + [JSInvokable] + public void OnMouseDownNode1(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActiveNode = 1; + } + } + + [JSInvokable] + public void OnMouseDownNode2(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActiveNode = 2; + } + } + + [JSInvokable] + public void OnMouseDownNode3(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActiveNode = 3; + } + } + + [JSInvokable] + public void OnMouseDownNode4(int button, bool altKey, bool ctrlKey, bool shiftKey) + { + if (Model != null) + { + Model.ActiveNode = 4; + } + } + + + public void Dispose() + { + if (Model != null) + { + Model.ControlNodePosittionChanged -= CaculateZoneAsync; + Model = null; + } + if (DotNetObj is not null) DotNetObj.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor.css new file mode 100644 index 0000000..6c49e9f --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneControlPoint.razor.css @@ -0,0 +1,31 @@ +circle { + cursor: default; + fill: #FFBD33; + stroke-width: 0; + r: var(--node-r); +} + + circle:hover { + stroke-width: 0.03px; + cursor: pointer; + stroke: green; + } + + circle:active { + stroke-width: 0.03px; + stroke: #FF5733; + } + + circle + text { + transition-delay: 0.1s; + font-size: 0.01em; + font-weight: normal; + } + +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + user-select: none; +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor new file mode 100644 index 0000000..2c12b96 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor @@ -0,0 +1,92 @@ +@inject IJSRuntime JSRuntime + + + + + + + + + + +@code { + private ElementReference Ref; + private ElementReference PointRef; + + + public double X1 { get; private set; } + public double Y1 { get; private set; } + public double X2 { get; private set; } + public double Y2 { get; private set; } + public double X3 { get; private set; } + public double Y3 { get; private set; } + public double X4 { get; private set; } + public double Y4 { get; private set; } + public double X5 { get; private set; } + public double Y5 { get; private set; } + + public int Step { get; private set; } + + public async Task CreateAsync(double x, double y) + { + if (Step == 0) + { + await Visible(); + Step = 1; + X1 = x; + Y1 = y; + } + else if (Step == 1) Step = 2; + else if (Step == 2) Step = 3; + else if (Step == 3) + { + X4 = x; + Y4 = y; + Step = 4; + await CalculatorZone(); + } + } + + public async Task Update(double x, double y) + { + if (Step == 1) + { + X2 = x; + Y2 = y; + } + else if (Step == 2) + { + X3 = x; + Y3 = y; + } + else if (Step == 3) + { + X4 = x; + Y4 = y; + } + await CalculatorZone(); + StateHasChanged(); + } + + private async Task CalculatorZone() + { + var points = $"{X1},{Y1} {X2},{Y2}"; + if (Step == 2) points += $" {X3},{Y3}"; + if (Step == 3) points += $" {X3},{Y3} {X4},{Y4}"; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", points); + } + + public async Task Visible() + { + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "visible"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", PointRef, "visibility", "visible"); + } + + public async Task Hidden() + { + Step = 0; + X1 = X2 = X3 = X4 = Y1 = Y2 = Y3 = Y4 = 0; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "visibility", "hidden"); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", PointRef, "visibility", "hidden"); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor.css b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor.css new file mode 100644 index 0000000..4123564 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Editor/Zone/ZoneCreating.razor.css @@ -0,0 +1,12 @@ +polygon { + fill: #c2c2a3; + stroke: none; + fill-opacity: 0.5; +} + +circle { + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementDefaultProperty.razor b/RobotNet.WebApp/Maps/Components/Element/ElementDefaultProperty.razor new file mode 100644 index 0000000..c0af240 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementDefaultProperty.razor @@ -0,0 +1,238 @@ +@inject IDialogService Dialog +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory + +
+
+ Default Properties +
+ + + + + + + + + +
+
+
+ @foreach (var property in ElementProperties) + { +
+ @if (property.Type == typeof(int).ToString()) + { + if (int.TryParse(property.DefaultValue, out int value)) + { + + } + } + else if (property.Type == typeof(double).ToString()) + { + if (double.TryParse(property.DefaultValue, out double value)) + { + + } + } + else if (property.Type == typeof(bool).ToString()) + { + if (bool.TryParse(property.DefaultValue, out bool value)) + { + + } + } + else if (property.Type == typeof(string).ToString()) + { + + } + + + +
+ } +
+
+ + + + Create Element Property + + +
+ + + + @foreach (var type in ElementProperty.PropertyTypes) + { + @ElementProperty.GetPropertyTypeName(type) + } + + + + +
+
+ + Cancel + Create + +
+ + +@code { + private bool CreatePropertyIsVisible; + private ElementProperty ElementPropertyCreateModel = new(); + + private ElementModelDto? ElementModel = null; + private List ElementProperties = []; + private bool createPropertyValidate; + private MudForm Form = default!; + + private bool IsEdit = false; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + private void OpenElementProperty() + { + ElementPropertyCreateModel.Name = ""; + ElementPropertyCreateModel.Type = ElementProperty.PropertyTypes[0].ToString(); + ElementPropertyCreateModel.DefaultValue = ""; + ElementPropertyCreateModel.ReadOnly = false; + + CreatePropertyIsVisible = true; + StateHasChanged(); + } + + public void SetElementModel(ElementModelDto? model) + { + ElementModel = model; + ElementProperties.Clear(); + if (model is not null && !string.IsNullOrEmpty(model.Content)) + { + var properties = JsonSerializer.Deserialize>(model.Content, JsonOptionExtends.Read); + if (properties is not null) ElementProperties = [.. properties]; + } + + StateHasChanged(); + } + + private async Task CreateProperty() + { + if (string.IsNullOrEmpty(ElementPropertyCreateModel.Name)) + { + Snackbar.Add("Name không được để trống.", Severity.Warning); + return; + } + if (ElementProperties.Any(p => p.Name == ElementPropertyCreateModel.Name)) + { + Snackbar.Add($"Tên Property {ElementPropertyCreateModel.Name} đã tồn tại, vui lòng chon tên khác!", Severity.Warning); + return; + } + ElementProperties.Add(new ElementProperty() + { + Id = Guid.NewGuid(), + Name = ElementPropertyCreateModel.Name, + Type = ElementPropertyCreateModel.Type, + DefaultValue = ElementPropertyCreateModel.DefaultValue, + ReadOnly = ElementPropertyCreateModel.ReadOnly, + }); + + await Save(); + CreatePropertyIsVisible = false; + StateHasChanged(); + } + + public IEnumerable DefaultValidation(string value) + { + if (ElementPropertyCreateModel.Type == typeof(int).ToString()) + { + if (!int.TryParse(value, out _)) yield return "Giá trị mặc định không hợp lệ"; + } + else if (ElementPropertyCreateModel.Type == typeof(double).ToString()) + { + if (!double.TryParse(value, out _)) yield return "Giá trị mặc định không hợp lệ"; + } + else if (ElementPropertyCreateModel.Type == typeof(bool).ToString()) + { + if (!bool.TryParse(value, out _)) yield return "Giá trị mặc định không hợp lệ"; + } + } + + private void DefaultIntValueChanged(Guid id, int value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultDoubleValueChanged(Guid id, double value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultBooleanValueChanged(Guid id, bool value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private async Task DeleteProperty(ElementProperty property) + { + var parameters = new DialogParameters + { + { x => x.Content, $"Bạn có chắc chắn muốn xóa property {property.Name} đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Property", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var prope = ElementProperties.Remove(property); + } + StateHasChanged(); + } + + private async Task Save() + { + if (ElementModel is null) return; + var result = await (await Http.PutAsJsonAsync($"api/ElementModels", new ElementModelUpdateModel() + { + Id = ElementModel.Id, + Name = ElementModel.Name, + MapId = ElementModel.MapId, + Width = ElementModel.Width, + Height = ElementModel.Height, + Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write), + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else Snackbar.Add("Cập nhật thành công", Severity.Success); + + IsEdit = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor b/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor new file mode 100644 index 0000000..d02fa24 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor @@ -0,0 +1,187 @@ +@using System.Net.Http.Headers + +@inject IJSRuntime JSRuntime +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory + +
+
+
+ Open +
+ + +
+
+
+ element model image +
+
+
+
+ Close +
+ + +
+
+
+ element model image +
+
+
+ + + + + Update ElementModel's Image + + + +
+
+
+
+
+ Element Model Image +
+
+
+
+ + Cancel + Update + +
+ +@code { + private bool Disable = true; + private string imageOpenSrc = "/images/Image-not-found.png"; + private string imageCloseSrc = "/images/Image-not-found.png"; + private string ElementModelName = "Element Model preview"; + private Guid ElementModelId = Guid.Empty; + + private string ImagePreview = ""; + private IBrowserFile? ElementImageChange; + private bool IsUpdateElementImageVisable; + private bool IsOpen; + + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + public void SetElementModel(ElementModelDto? model) + { + if (model is null) + { + Disable = true; + ElementModelName = "Element Model preview"; + imageOpenSrc = "/images/Image-not-found.png"; + imageCloseSrc = "/images/Image-not-found.png"; + ElementModelId = Guid.Empty; + } + else + { + imageOpenSrc = $"{Http.BaseAddress}api/images/elementmodel/{model.Id}?IsOpen=true&t={DateTime.Now}"; + imageCloseSrc = $"{Http.BaseAddress}api/images/elementmodel/{model.Id}?IsOpen=false&t={DateTime.Now}"; + ElementModelName = model.Name; + ElementModelId = model.Id; + Disable = false; + } + + StateHasChanged(); + } + + private async Task DownloadImage(bool isOpen) + { + try + { + var response = await Http.GetAsync(isOpen ? imageOpenSrc : imageCloseSrc); + + if (response.IsSuccessStatusCode) + { + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + + var base64Data = Convert.ToBase64String(fileBytes); + var mimeType = "image/png"; + var url = $"data:{mimeType};base64,{base64Data}"; + + await JSRuntime.InvokeVoidAsync("DownloadImage", base64Data, $"{ElementModelName}-{(isOpen ? "O" : "C")}.png"); + } + else Snackbar.Add("Không thể tải ảnh robot", Severity.Warning); + } + catch (Exception ex) + { + Snackbar.Add($"Không thể tải ảnh robot: {ex.Message}", Severity.Warning); + } + } + + private void OpenUpdateElementImage(bool isOpen) + { + IsOpen = isOpen; + ImagePreview = isOpen ? imageOpenSrc : imageCloseSrc; + ElementImageChange = null; + IsUpdateElementImageVisable = true; + StateHasChanged(); + } + + public async Task UploadMedia(IBrowserFile file) + { + var path = Path.Combine(Path.GetTempPath(), ElementModelName); + await using var fs = new FileStream(path, FileMode.Create); + await file.OpenReadStream(file.Size).CopyToAsync(fs); + var bytes = new byte[file.Size]; + fs.Position = 0; + await fs.ReadAsync(bytes); + fs.Close(); + File.Delete(path); + return $"data:{file.ContentType};base64,{Convert.ToBase64String(bytes)}"; + } + + private async Task ElementImageChanged(InputFileChangeEventArgs e) + { + ElementImageChange = e.File; + if (ElementImageChange is not null) + { + ImagePreview = await UploadMedia(ElementImageChange); + StateHasChanged(); + } + } + + private async Task UpdateElementModelImage() + { + if (ElementImageChange is null) return; + + using var content = new MultipartFormDataContent(); + var fileContent = new StreamContent(ElementImageChange.OpenReadStream()); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(ElementImageChange.ContentType); + content.Add(fileContent, "image", ElementImageChange.Name); + + var result = await (await Http.PutAsync($"api/ElementModels/{(IsOpen ? "openimage" : "closeimage")}/{ElementModelId}", content)).Content.ReadFromJsonAsync(); + if (result is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + if (IsOpen) imageOpenSrc = ImagePreview; + else imageCloseSrc = ImagePreview; + + IsUpdateElementImageVisable = false; + Snackbar.Add("Thay đổi thành công", Severity.Success); + } + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor.css b/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor.css new file mode 100644 index 0000000..e67f74e --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementImage.razor.css @@ -0,0 +1,31 @@ +.element-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .element-item img { + justify-content: center; + width: 95%; + height: 200px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } + + +.element-update-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .element-update-item img { + width: 400px; + height: 300px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor b/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor new file mode 100644 index 0000000..2df51fb --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor @@ -0,0 +1,341 @@ +@using System.Net.Http.Headers +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory +@inject IDialogService Dialog + +
+
+ Element Models +
+ + + + + + + + + +
+
+
+ + @foreach (var model in ElementModels) + { + + } + +
+
+ + + + Create Element Model + + +
+ +
+ + +
+
+
+
+
+
+
+ Element Model Image Open +
+
+
+
+
+
+
+
+  Element Model Image Close +
+
+
+
+
+
+ + Cancel + Create + +
+ + + + Update Element Model + + +
+ +
+ + +
+
+
+ + Cancel + Update + +
+ + +@code { + [Parameter] + public List ElementModels { get; set; } = []; + + [Parameter] + public MapInfoDto MapInfo { get; set; } = new(); + + [Parameter] + public EventCallback ElementModelSelectChanged { get; set; } + + private string? txtSearch { get; set; } + + private bool CreateModelIsVisible; + private ElementModelCreateModel ElementModelCreateModel = new(); + private IBrowserFile? ElementModelCreateImage1 { get; set; } + private string? ProjectionImageFilePreview1 { get; set; } + private IBrowserFile? ElementModelCreateImage2 { get; set; } + private string? ProjectionImageFilePreview2 { get; set; } + + private ElementModelDto? ElementModelSelected = null; + + private bool UpdateElementModelVisible; + private ElementModelUpdateModel ElementModelUpdateModel = new(); + + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + private async Task DeleteElementModel() + { + if (ElementModelSelected is null) return; + + var parameters = new DialogParameters + { + { x => x.Content, $"Tất cả các Element thuộc model này cũng sẽ bị xóa! Bạn có chắc chắn muốn xóa element model {ElementModelSelected.Name} đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Element Model", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var response = await Http.DeleteFromJsonAsync($"api/ElementModels/{ElementModelSelected.Id}"); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var elementmodel = ElementModels.FirstOrDefault(m => m.Id == ElementModelSelected.Id); + if (elementmodel is not null) + { + ElementModels.Remove(elementmodel); + await ElementModelSelectChanged.InvokeAsync(null); + ElementModelSelected = null; + Snackbar.Add($"Xóa element model thành công.", Severity.Success); + } + else Snackbar.Add($"Element Model không tồn tại.", Severity.Warning); + } + StateHasChanged(); + } + } + + private void OpenCreateModel() + { + ElementModelCreateModel.Name = ""; + ElementModelCreateModel.MapId = MapInfo.Id; + ProjectionImageFilePreview1 = "/images/Image-not-found.png"; + ProjectionImageFilePreview2 = "/images/Image-not-found.png"; + ElementModelCreateImage1 = null; + ElementModelCreateImage2 = null; + + CreateModelIsVisible = true; + StateHasChanged(); + } + + private async Task CreateElementModel() + { + try + { + if (ElementModelCreateImage1 is null || string.IsNullOrEmpty(ProjectionImageFilePreview1)) + { + Snackbar.Add("Ảnh model chưa được chọn.", Severity.Warning); + return; + } + if (ElementModelCreateImage2 is null || string.IsNullOrEmpty(ProjectionImageFilePreview2)) + { + Snackbar.Add("Ảnh model chưa được chọn.", Severity.Warning); + return; + } + + if (string.IsNullOrEmpty(ElementModelCreateModel.Name)) + { + Snackbar.Add("Name không được để trống.", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(ElementModelCreateModel.Name); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + using var content = new MultipartFormDataContent + { + { new StringContent(ElementModelCreateModel.MapId.ToString()), nameof(ElementModelCreateModel.MapId) }, + { new StringContent(ElementModelCreateModel.Name ?? String.Empty), nameof(ElementModelCreateModel.Name) }, + { new StringContent(ElementModelCreateModel.Width.ToString()), nameof(ElementModelCreateModel.Width) }, + { new StringContent(ElementModelCreateModel.Height.ToString()), nameof(ElementModelCreateModel.Height) }, + }; + + var fileContent1 = new StreamContent(ElementModelCreateImage1.OpenReadStream()); + fileContent1.Headers.ContentType = new MediaTypeHeaderValue(ElementModelCreateImage1.ContentType); + content.Add(fileContent1, "imageOpen", ElementModelCreateImage1.Name); + + var fileContent2 = new StreamContent(ElementModelCreateImage2.OpenReadStream()); + fileContent2.Headers.ContentType = new MediaTypeHeaderValue(ElementModelCreateImage2.ContentType); + content.Add(fileContent2, "imageClose", ElementModelCreateImage2.Name); + + var response = await (await Http.PostAsync("api/ElementModels", content)).Content.ReadFromJsonAsync>(); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else if (response.Data is null) Snackbar.Add("Hệ thống không thể tạo Element model này", Severity.Error); + else + { + ElementModels.Add(response.Data); + Snackbar.Add($"Tạo Element model {response.Data.Name} thành công.", Severity.Success); + } + + CreateModelIsVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task ElementModelCreateImage1Changed(InputFileChangeEventArgs e) + { + ElementModelCreateImage1 = e.File; + if (ElementModelCreateImage1 is not null) + { + var buffers = new byte[ElementModelCreateImage1.Size]; + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + await using var fs = new FileStream(path, FileMode.Create); + await ElementModelCreateImage1.OpenReadStream(ElementModelCreateImage1.Size).CopyToAsync(fs); + fs.Position = 0; + await fs.ReadAsync(buffers); + fs.Close(); + File.Delete(path); + + ProjectionImageFilePreview1 = $"data:{ElementModelCreateImage1.ContentType};base64,{Convert.ToBase64String(buffers)}"; + StateHasChanged(); + } + } + + private async Task ElementModelCreateImage2Changed(InputFileChangeEventArgs e) + { + ElementModelCreateImage2 = e.File; + if (ElementModelCreateImage2 is not null) + { + var buffers = new byte[ElementModelCreateImage2.Size]; + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + await using var fs = new FileStream(path, FileMode.Create); + await ElementModelCreateImage2.OpenReadStream(ElementModelCreateImage2.Size).CopyToAsync(fs); + fs.Position = 0; + await fs.ReadAsync(buffers); + fs.Close(); + File.Delete(path); + + ProjectionImageFilePreview2 = $"data:{ElementModelCreateImage2.ContentType};base64,{Convert.ToBase64String(buffers)}"; + StateHasChanged(); + } + } + + private async Task ModelSelectedChanged(ElementModelDto model) + { + ElementModelSelected = model; + var elModel = await Http.GetFromJsonAsync>($"api/ElementModels/{ElementModelSelected.Id}"); + if (elModel is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!elModel.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elModel.Message}", Severity.Error); + else await ElementModelSelectChanged.InvokeAsync(elModel.Data); + } + + private void OpenUpdateElementModel() + { + if (ElementModelSelected is null) return; + ElementModelUpdateModel.Id = ElementModelSelected.Id; + ElementModelUpdateModel.Name = ElementModelSelected.Name; + ElementModelUpdateModel.Width = ElementModelSelected.Width; + ElementModelUpdateModel.Height = ElementModelSelected.Height; + ElementModelUpdateModel.MapId = ElementModelSelected.MapId; + ElementModelUpdateModel.Content = ElementModelSelected.Content; + + UpdateElementModelVisible = true; + StateHasChanged(); + } + + private async Task UpdateElementModel() + { + var result = await (await Http.PutAsJsonAsync($"api/ElementModels", new ElementModelUpdateModel() + { + Id = ElementModelUpdateModel.Id, + Name = ElementModelUpdateModel.Name, + Width = ElementModelUpdateModel.Width, + Height = ElementModelUpdateModel.Height, + MapId = ElementModelUpdateModel.MapId, + Content = ElementModelUpdateModel.Content, + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error); + else + { + var elementModel = ElementModels.FirstOrDefault(m => m.Id == result.Data.Id); + if (elementModel is not null) + { + elementModel.Name = result.Data.Name; + elementModel.Width = result.Data.Width; + elementModel.Height = result.Data.Height; + } + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + + UpdateElementModelVisible = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor.css b/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor.css new file mode 100644 index 0000000..51926c8 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementModelTable.razor.css @@ -0,0 +1,16 @@ +.model-create-item { + display: flex; + justify-content: center; + width: 100%; + height: 200px; +} + + .model-create-item img { + justify-content: center; + width: 200px; + height: 200px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + border: 0.2px solid gray; + } diff --git a/RobotNet.WebApp/Maps/Components/Element/ElementPropertyTable.razor b/RobotNet.WebApp/Maps/Components/Element/ElementPropertyTable.razor new file mode 100644 index 0000000..06cb106 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/ElementPropertyTable.razor @@ -0,0 +1,193 @@ +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +
+
+ Properties +
+ + + + + + + + + +
+
+
+ @foreach (var property in ElementProperties) + { +
+ @if (property.Type == typeof(int).ToString()) + { + if (int.TryParse(property.DefaultValue, out int value)) + { + + } + } + else if (property.Type == typeof(double).ToString()) + { + if (double.TryParse(property.DefaultValue, out double value)) + { + + } + } + else if (property.Type == typeof(bool).ToString()) + { + if (bool.TryParse(property.DefaultValue, out bool value)) + { + + } + } + else if (property.Type == typeof(string).ToString()) + { + + } +
+ } +
+
+ +@code { + [Parameter] + public EventCallback ElementPropertyChagned { get; set; } + + private List ElementProperties = []; + + private bool IsEdit = false; + private ElementDto? Element = null; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + public void SetProperties(ElementDto? element) + { + ElementProperties.Clear(); + Element = element; + if (element is not null && !string.IsNullOrEmpty(element.Content)) + { + var properties = JsonSerializer.Deserialize>(element.Content, JsonOptionExtends.Read); + if (properties is not null) ElementProperties.AddRange(properties); + } + StateHasChanged(); + } + + private void DefaultIntValueChanged(Guid id, int value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultDoubleValueChanged(Guid id, double value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultBooleanValueChanged(Guid id, bool value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private async Task Save() + { + if (Element is null) return; + + var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel() + { + Id = Element.Id, + Name = Element.Name, + IsOpen = Element.IsOpen, + OffsetX = Element.OffsetX, + OffsetY = Element.OffsetY, + Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write), + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error); + else + { + await ElementPropertyChagned.InvokeAsync(result.Data); + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + + IsEdit = false; + StateHasChanged(); + } + + private async Task SynchronizeProperty() + { + if (Element is null) return; + var parameters = new DialogParameters + { + { x => x.Content, $"Bạn có chắc chắn muốn đồng bộ tham số với Element Model không?" }, + { x => x.ConfirmText, "Yes" }, + { x => x.Color, Color.Primary } + }; + var confirm = await Dialog.ShowAsync("Đồng bộ tham số", parameters); + var result = await confirm.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var elementModel = await LoadElementModels(Element.ModelId); + if (elementModel is null) Snackbar.Add("Element Model không tồn tại", Severity.Error); + else + { + List ModelProperties = []; + var modelProperties = JsonSerializer.Deserialize>(elementModel.Content, JsonOptionExtends.Read); + if (modelProperties is not null) ModelProperties.AddRange(modelProperties); + parameters = new DialogParameters + { + { x => x.Content, $"Bạn có muốn đồng bộ giá trị với Element Model không?" }, + { x => x.CancelText, "No" }, + { x => x.ConfirmText, "Yes" }, + { x => x.Color, Color.Primary } + }; + confirm = await Dialog.ShowAsync("Đồng bộ tham số", parameters); + result = await confirm.Result; + if (result is not null) + { + var valueSynchronize = result.Data is not null && bool.TryParse(result.Data.ToString(), out _); + foreach (var modelProperty in ModelProperties) + { + var property = ElementProperties.FirstOrDefault(em => em.Id == modelProperty.Id); + if(property is null) ElementProperties.Add(modelProperty); + else + { + property.Name = modelProperty.Name; + property.DefaultValue = valueSynchronize ? modelProperty.DefaultValue : property.DefaultValue; + } + } + ElementProperties.RemoveAll(ep => !ModelProperties.Any(mp => mp.Id == ep.Id)); + await Save(); + } + } + StateHasChanged(); + } + } + + private async Task LoadElementModels(Guid modelId) + { + var elModel = await Http.GetFromJsonAsync>($"api/ElementModels/{modelId}"); + if (elModel is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!elModel.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elModel.Message}", Severity.Error); + else return elModel.Data; + return null; + } +} diff --git a/RobotNet.WebApp/Maps/Components/Element/MapElement.razor b/RobotNet.WebApp/Maps/Components/Element/MapElement.razor new file mode 100644 index 0000000..57d5788 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Element/MapElement.razor @@ -0,0 +1,288 @@ +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar +@inject IDialogService Dialog +@inject NavigationManager Nav + +
+
+ Elements + + + + + +
+
+ + + Name + Model + IsOpen + OffsetX + OffsetY + NodeId + + + + @context.Name + @context.ModelName + @context.IsOpen + @context.OffsetX + @context.OffsetY + @context.NodeId + +
+ + Edit + Delete + +
+
+
+ + + +
+
+
+ + + + + + + Update Element @ElementUpdateModel.Name + + + +
+ + + + +
+
+ + Cancel + Update + +
+ +@code { + [Parameter] + public EventCallback ElementSelectChanged { get; set; } + + private List Elements { get; set; } = []; + private List ElementsShow { get; set; } = []; + + private string txtSearch = ""; + private bool IsLoading; + private MudTable Table = default!; + private int selectedRowNumber = -1; + + private bool updateElementVisble; + private ElementUpdateModel ElementUpdateModel = new(); + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + private bool FilterFunc(ElementDto element) + { + if (string.IsNullOrWhiteSpace(txtSearch)) + return true; + if (element.Name.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + private Task> ReloadData(TableState state, CancellationToken _) + { + var robots = new List(); + ElementsShow.Clear(); + Elements.ForEach(robot => + { + if (FilterFunc(robot)) robots.Add(robot); + }); + ElementsShow = robots.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); + return Task.FromResult(new TableData() { TotalItems = robots.Count(), Items = ElementsShow }); + } + + private async Task LoadElements(Guid modelId) + { + IsLoading = true; + Elements.Clear(); + StateHasChanged(); + + var elements = await Http.GetFromJsonAsync>>($"api/Elements/{modelId}"); + if (elements is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!elements.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elements.Message}", Severity.Error); + else if (elements.Data is null || !elements.Data.Any()) + { + Snackbar.Add("Không có element nào trong model này.", Severity.Warning); + ElementsShow.Clear(); + Table?.ReloadServerData(); + } + else + { + Elements.AddRange(elements.Data.OrderBy(el => el.Name)); + Table?.ReloadServerData(); + } + IsLoading = false; + StateHasChanged(); + } + + public async Task SetData(ElementModelDto? model) + { + selectedRowNumber = -1; + Table.SetSelectedItem(null); + _ = ElementSelectChanged.InvokeAsync(null); + if (model is null) + { + Elements.Clear(); + await Table.ReloadServerData(); + } + else await LoadElements(model.Id); + } + + private void RowClickEvent(TableRowClickEventArgs tableRowClickEventArgs) { } + + private string SelectedRowClassFunc(ElementDto element, int rowNumber) + { + if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && !Table.SelectedItem.Equals(element)) + { + return string.Empty; + } + else if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + return "selected"; + } + else if (Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + selectedRowNumber = rowNumber; + _ = ElementSelectChanged.InvokeAsync(element); + return "selected"; + } + else + { + return string.Empty; + } + } + + private void TextSearchChanged(string text) + { + txtSearch = text; + Table?.ReloadServerData(); + } + + private async Task Delete(ElementDto element) + { + var parameters = new DialogParameters + { + { x => x.Content, $"Bạn có chắc chắn muốn xóa element {element.Name} đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Element", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var response = await Http.DeleteFromJsonAsync($"api/Elements/{element.Id}"); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var elementData = Elements.FirstOrDefault(m => m.Id == element.Id); + if (elementData is not null) + { + Elements.Remove(elementData); + await Table.ReloadServerData(); + await ElementSelectChanged.InvokeAsync(null); + selectedRowNumber = -1; + Snackbar.Add($"Xóa element thành công.", Severity.Success); + } + else Snackbar.Add($"Element không tồn tại.", Severity.Warning); + } + StateHasChanged(); + } + } + + private void OpenEditElement(ElementDto model) + { + ElementUpdateModel.Id = model.Id; + ElementUpdateModel.Name = model.Name; + ElementUpdateModel.IsOpen = model.IsOpen; + ElementUpdateModel.OffsetX = model.OffsetX; + ElementUpdateModel.OffsetY = model.OffsetY; + ElementUpdateModel.Content = model.Content; + + updateElementVisble = true; + StateHasChanged(); + } + + private async Task UpdateElement() + { + var selectedElement = Elements.FirstOrDefault(e => e.Id == ElementUpdateModel.Id); + if (selectedElement is null) return; + + var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel() + { + Id = ElementUpdateModel.Id, + Name = ElementUpdateModel.Name, + IsOpen = ElementUpdateModel.IsOpen, + OffsetX = ElementUpdateModel.OffsetX, + OffsetY = ElementUpdateModel.OffsetY, + Content = ElementUpdateModel.Content, + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error); + else + { + selectedElement.Name = result.Data.Name; + selectedElement.OffsetX = result.Data.OffsetX; + selectedElement.OffsetY = result.Data.OffsetY; + selectedElement.IsOpen = result.Data.IsOpen; + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + + updateElementVisble = false; + StateHasChanged(); + } + + public void UpdateProperty(ElementDto model) + { + var element = Elements.FirstOrDefault(e => e.Id == model.Id); + if (element is null) return; + + element.Name = model.Name; + element.OffsetX = model.OffsetX; + element.OffsetY = model.OffsetY; + element.IsOpen = model.IsOpen; + element.Content = model.Content; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor b/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor new file mode 100644 index 0000000..2ef7196 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor @@ -0,0 +1,207 @@ +@using System.Net.Http.Headers +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + +@inject IHttpClientFactory HttpClientFactory +@inject IJSRuntime JSRuntime +@inject NavigationManager Nav +@inject ISnackbar Snackbar + +
+ @MapName +
+
+ map image +
+
+ Map's Image +
+ + +
+
+ + Design + + + Setting + + + Element + + + Export + + + @* *@ +
+
+ + + + + Update Map's Image + + + +
+
+
+
+
+ Map Image +
+
+
+
+ + Cancel + Update + +
+ +@code { + [Parameter] + public EventCallback MapImageChangedCallBack { get; set; } + + private bool Disable = true; + private string imageSrc = "/images/Image-not-found.png"; + private string MapName = "Map preview"; + private Guid MapId = Guid.Empty; + + private string ImagePreview = ""; + private IBrowserFile? MapImageChange; + + private bool IsUpdateMapImageVisable; + + private HttpClient Http = default!; + + protected override void OnInitialized() + { + base.OnInitialized(); + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + } + + public void SetMapPreview(MapInfoDto? map) + { + if (map is null) + { + Disable = true; + MapName = "Map preview"; + imageSrc = "/images/Image-not-found.png"; + MapId = Guid.Empty; + } + else + { + imageSrc = $"{Http.BaseAddress}api/Images/map/{map.Id}?t={DateTime.Now}"; + MapName = map.Name; + MapId = map.Id; + Disable = false; + } + StateHasChanged(); + } + + private async Task DownloadImage() + { + try + { + var response = await Http.GetAsync(imageSrc); + + if (response.IsSuccessStatusCode) + { + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + + var base64Data = Convert.ToBase64String(fileBytes); + var mimeType = "image/png"; + var url = $"data:{mimeType};base64,{base64Data}"; + + await JSRuntime.InvokeVoidAsync("DownloadImage", url, $"{MapName}.png"); + } + else Snackbar.Add("Không thể tải ảnh map", Severity.Warning); + } + catch (Exception ex) + { + Snackbar.Add($"Không thể tải ảnh map: {ex.Message}", Severity.Warning); + } + } + + private void OpenUpdateMapImage() + { + ImagePreview = imageSrc; + MapImageChange = null; + IsUpdateMapImageVisable = true; + StateHasChanged(); + } + + public async Task UploadMedia(IBrowserFile file) + { + var path = Path.Combine(Path.GetTempPath(), MapName); + await using var fs = new FileStream(path, FileMode.Create); + await file.OpenReadStream(file.Size).CopyToAsync(fs); + var bytes = new byte[file.Size]; + fs.Position = 0; + await fs.ReadAsync(bytes); + fs.Close(); + File.Delete(path); + return $"data:{file.ContentType};base64,{Convert.ToBase64String(bytes)}"; + } + + private async Task MapImageChanged(InputFileChangeEventArgs e) + { + MapImageChange = e.File; + if (MapImageChange is not null) + { + ImagePreview = await UploadMedia(MapImageChange); + StateHasChanged(); + } + } + + private async Task UpdateMapImage() + { + if (MapImageChange is null) return; + + using var content = new MultipartFormDataContent(); + var fileContent = new StreamContent(MapImageChange.OpenReadStream(maxAllowedSize: 20480000)); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(MapImageChange.ContentType); + content.Add(fileContent, "image", MapImageChange.Name); + var result = await (await Http.PutAsync($"api/MapsManager/image/{MapId}", content)).Content.ReadFromJsonAsync(); + if (result is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + imageSrc = ImagePreview; + await MapImageChangedCallBack.InvokeAsync(MapId); + IsUpdateMapImageVisable = false; + Snackbar.Add("Thay đổi thành công", Severity.Success); + } + StateHasChanged(); + } + + private async Task ExportMap() + { + try + { + if (MapId == Guid.Empty) return; + var response = await Http.GetAsync($"api/MapExport/encrypt/{MapId}"); + if (response is not null && response.IsSuccessStatusCode) + { + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + var fileName = response.Content.Headers.ContentDisposition?.FileName; + using var streamRef = new DotNetStreamReference(stream: new MemoryStream(fileBytes)); + await JSRuntime.InvokeVoidAsync("DownloadMap", string.IsNullOrEmpty(fileName) ? $"{MapName}.map" : fileName, streamRef); + } + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } +} diff --git a/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor.css b/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor.css new file mode 100644 index 0000000..ac1ded4 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/NavigationMapPreview.razor.css @@ -0,0 +1,42 @@ +.map-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .map-item img { + justify-content: center; + width: 95%; + height: 400px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } + +.title { + display: flex; + height: 77px; + width: 100%; + justify-content: center; + align-content: center; + /*border-bottom: 0.5px solid gray;*/ + font-size: 30px; + flex-wrap: wrap; + padding-bottom: .5rem; +} + +.map-update-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .map-update-item img { + width: 100%; + height: 400px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } diff --git a/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor b/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor new file mode 100644 index 0000000..1b83334 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor @@ -0,0 +1,390 @@ +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +
+
+ + @foreach (var action in Actions) + { + + + @action.Name + + + } + +
+ +
+
+ Action: @ActionSelected?.Name +
+ +
+ + + + + + +
+ +
+
+ + +
+
+
+ Add Parameter +
+
+
+
+ + + + + + + + + + + + + +
+ +
+
+
+
+
+
+
+
+
+ +
+ @ActionSelected?.Content +
+
+
+
+ + + + +
+ + + + Create Action + + + + + + Cancel + Create + + + +@code { + [Parameter, EditorRequired] + public Guid Id { get; set; } + + private bool CreateActionVisible = false; + + private ActionCreateModel ActionCreateModel = new(); + + private List Actions = []; + private ActionDto? ActionSelected = null; + private readonly ActionModel VDAActionSelected = new(); + private readonly ActionModel VDAActionSelectedReset = new(); + + private bool OverlayVisible; + private ActionParameter ActionParameter = new(); + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + await LoadAction(); + StateHasChanged(); + } + + private async Task LoadAction() + { + OverlayVisible = true; + Actions.Clear(); + var result = await Http.GetFromJsonAsync>($"api/Actions/{Id}"); + + if (result is not null && result.Any()) + { + Actions.AddRange(result); + ActionSelectedChanged(Actions.First()); + } + + OverlayVisible = false; + StateHasChanged(); + } + + private async Task CreateAction() + { + var nameInvalid = MapEditorHelper.NameValidation(ActionCreateModel.Name); + if (nameInvalid.Any()) + { + Snackbar.Add(string.Join(';', nameInvalid), Severity.Error); + return; + } + var actionDefault = new ActionModel() + { + ActionId = Guid.NewGuid().ToString(), + ActionType = ActionCreateModel.Name, + ActionDescription = "", + BlockingType = BlockingType.NONE.ToString(), + ActionParameters = [], + }; + ActionCreateModel.Content = JsonSerializer.Serialize(actionDefault, JsonOptionExtends.Write); + + var result = await (await Http.PostAsJsonAsync("api/Actions", ActionCreateModel)).Content.ReadFromJsonAsync>(); + + if (result == null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + else if (result.Data is null) + { + Snackbar.Add("Không có dữ liệu trả về", Severity.Error); + return; + } + + Actions.Add(result.Data); + if (Actions.Any()) ActionSelectedChanged(Actions.First()); + else ActionSelected = new(); + + Snackbar.Add("Tạo Action thành công", Severity.Success); + CreateActionVisible = false; + StateHasChanged(); + } + + public async Task DeleteAction() + { + if (ActionSelected is null) return; + var parameters = new DialogParameters + { + { x => x.Content, $"Bạn có chắc chắn muốn xóa Action {ActionSelected.Name} đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Action", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var deletResult = await Http.DeleteFromJsonAsync($"api/Actions/{ActionSelected.Id}"); + + if (deletResult == null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!deletResult.IsSuccess) + { + Snackbar.Add(deletResult.Message, Severity.Error); + return; + } + + var action = Actions.FirstOrDefault(action => action.Id == ActionSelected.Id); + if (action is not null) Actions.Remove(action); + if (Actions.Any()) ActionSelectedChanged(Actions.First()); + else + { + ActionSelected = null; + ActionModel actionNew = new(); + SetActionSelectedDto(actionNew); + SetActionSelectedResetDto(actionNew); + } + + Snackbar.Add("Xóa Action thành công", Severity.Success); + StateHasChanged(); + } + } + + private void ActionSelectedChanged(ActionDto action) + { + if (ActionSelected != action) + { + ActionSelected = action; + var vdaAction = JsonSerializer.Deserialize(ActionSelected.Content, JsonOptionExtends.Read); + if (vdaAction is not null) + { + SetActionSelectedDto(vdaAction); + SetActionSelectedResetDto(vdaAction); + } + StateHasChanged(); + } + } + + public void ShowCreateAction() + { + ActionCreateModel.MapId = Id; + ActionCreateModel.Content = ""; + CreateActionVisible = true; + StateHasChanged(); + } + + public void Reset() + { + if (ActionSelected is null) return; + SetActionSelectedDto(VDAActionSelectedReset); + PreviewAction(); + StateHasChanged(); + } + + public async Task Save() + { + if (ActionSelected is null) return; + if (!ValidateUpdateData(VDAActionSelectedReset, VDAActionSelected)) + { + Snackbar.Add("Không có sự thay đổi", Severity.Warning); + return; + } + ActionSelected.Content = JsonSerializer.Serialize(VDAActionSelected, JsonOptionExtends.Write); + + var result = await (await Http.PutAsJsonAsync("api/Actions", ActionSelected)).Content.ReadFromJsonAsync(); + + if (result == null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + SetActionSelectedResetDto(VDAActionSelected); + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + + private void PreviewAction() + { + if (ActionSelected is null) return; + ActionSelected.Content = JsonSerializer.Serialize(VDAActionSelected, JsonOptionExtends.Write); + StateHasChanged(); + } + + private void SetActionSelectedDto(ActionModel dto) + { + VDAActionSelected.ActionId = dto.ActionId; + VDAActionSelected.ActionType = dto.ActionType; + VDAActionSelected.ActionDescription = dto.ActionDescription; + VDAActionSelected.BlockingType = dto.BlockingType; + List actionParameters = []; + foreach (var param in dto.ActionParameters) + { + actionParameters.Add(new() { Value = param.Value, Key = param.Key }); + } + VDAActionSelected.ActionParameters = [..actionParameters]; + } + + private void SetActionSelectedResetDto(ActionModel dto) + { + VDAActionSelectedReset.ActionId = dto.ActionId; + VDAActionSelectedReset.ActionType = dto.ActionType; + VDAActionSelectedReset.ActionDescription = dto.ActionDescription; + VDAActionSelectedReset.BlockingType = dto.BlockingType; + List actionParameters = []; + foreach (var param in dto.ActionParameters) + { + actionParameters.Add(new() { Value = param.Value, Key = param.Key }); + } + VDAActionSelectedReset.ActionParameters = [.. actionParameters]; + } + + private bool ValidateUpdateData(ActionModel old, ActionModel update) + { + if (old.ActionId != update.ActionId) return true; + if (old.ActionType != update.ActionType) return true; + if (old.ActionDescription != update.ActionDescription) return true; + if (old.BlockingType != update.BlockingType) return true; + if (old.ActionParameters.Length != update.ActionParameters.Length) return true; + foreach (var param in old.ActionParameters) + { + if (!update.ActionParameters.Any(a => a.Key == param.Key && a.Value == param.Value)) return true; + } + return false; + } + + private void AddParamterClick() + { + if (ActionSelected is null) return; + if (string.IsNullOrEmpty(ActionParameter.Key)) + { + Snackbar.Add("Key không được để trống", Severity.Warning); + return; + } + if (string.IsNullOrEmpty(ActionParameter.Value)) + { + Snackbar.Add("Value không được để trống", Severity.Warning); + return; + } + + List parameters = []; + if (VDAActionSelected.ActionParameters is not null && VDAActionSelected.ActionParameters.Any()) parameters.AddRange(VDAActionSelected.ActionParameters.ToList()); + parameters.Add(new() + { + Key = ActionParameter.Key, + Value = ActionParameter.Value + }); + VDAActionSelected.ActionParameters = [.. parameters]; + PreviewAction(); + ActionParameter.Key = string.Empty; + ActionParameter.Value = string.Empty; + StateHasChanged(); + } + + private void CommittedItemChanges(ActionParameter item) + { + PreviewAction(); + StateHasChanged(); + } + + private async Task DeleteParameter(ActionParameter item) + { + var parameters = new DialogParameters + { + { x => x.Content, "Bạn có chắc chắn muốn xóa paramter đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Paramter", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + List actionParameters = []; + if (VDAActionSelected.ActionParameters is not null && VDAActionSelected.ActionParameters.Any()) actionParameters.AddRange(VDAActionSelected.ActionParameters.ToList()); + if (actionParameters.Any()) actionParameters.Remove(item); + VDAActionSelected.ActionParameters = [.. actionParameters]; + + PreviewAction(); + StateHasChanged(); + } + } +} diff --git a/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor.css b/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor.css new file mode 100644 index 0000000..85c1b65 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Setting/MapSettingAction.razor.css @@ -0,0 +1,24 @@ +.actions-list { + display: flex; + flex-direction: column; + height: 100%; + width: 250px; + justify-content: flex-start; + align-items: flex-start; + overflow-y: auto; + overflow-x: hidden; +} + +.actions-preview { + height: 100%; + width: 40%; + overflow: hidden; + padding: 10px; +} + + .actions-preview div { + white-space: pre; + overflow: auto; + font-size: 17px; + height: 100%; + } diff --git a/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor b/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor new file mode 100644 index 0000000..5af989c --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor @@ -0,0 +1,277 @@ +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + +
+ + + Edge + + +
+ + + + + + + +
+ + Chiều dài tối thiểu khi tạo Edge (m) + + +
+
+ + Vận tốc lớn nhất cho phép khi di chuyển trên Edge (thẳng) (m/s) + + +
+
+ + Vận tốc lớn nhất cho phép khi di chuyển trên Edge (cong) (m/s) + + +
+
+ + Chiều cao lớn nhất cho phép di chuyển trên Edge (m) + + +
+
+ + Chiều cao tối thiểu cho phép di chuyển trên Edge (m) + + +
+ +
+ + Vận tốc quay lớn nhất cho phép di chuyển trên Edge (rad/s) + + +
+ +
+ + Sai số cho phép về tọa độ X,Y khi đi qua Edge (m) + + +
+ +
+ + Sai số cho phép về góc khi đi qua Edge (degree) + + +
+ +
+ +
+
+
+
+ + + + Node + + +
+
+ + Sai số cho phép về tọa độ X,Y khi đi qua Node (m) + + +
+ +
+ + Sai số cho phép về góc khi đi qua Node (degree) + + +
+ +
+ + +
+
+
+
+ + + + Zone + + +
+
+ + Diện tích tối thiểu khi tạo Zone (m2) + + +
+
+
+
+ + + + +
+ + +@code { + [Parameter, EditorRequired] + public Guid Id { get; set; } + + private readonly MapSettingDefaultDto MapSettingDefaultDto = new(); + private readonly MapSettingDefaultDto MapSettingDefaultResetDto = new(); + + private bool OverlayVisible; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + await LoadMapVersionConfig(); + StateHasChanged(); + } + + private async Task LoadMapVersionConfig() + { + OverlayVisible = true; + StateHasChanged(); + + var result = await Http.GetFromJsonAsync>($"api/MapsSetting/{Id}"); + + if (result == null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + if (result.Data == null) + { + Snackbar.Add("Không tìm thấy cấu hình mặc định cho bản đồ này", Severity.Error); + return; + } + + SetConfigBindingDto(result.Data); + SetConfigResetDto(result.Data); + + OverlayVisible = false; + StateHasChanged(); + } + + private void SetConfigResetDto(MapSettingDefaultDto dto) + { + MapSettingDefaultResetDto.Id = dto.Id; + + MapSettingDefaultResetDto.EdgeMinLength = dto.EdgeMinLength; + MapSettingDefaultResetDto.EdgeMinHeight = dto.EdgeMinHeight; + MapSettingDefaultResetDto.EdgeMaxHeight = dto.EdgeMaxHeight; + MapSettingDefaultResetDto.EdgeMaxRotationSpeed = dto.EdgeMaxRotationSpeed; + MapSettingDefaultResetDto.EdgeStraightMaxSpeed = dto.EdgeStraightMaxSpeed; + MapSettingDefaultResetDto.EdgeCurveMaxSpeed = dto.EdgeCurveMaxSpeed; + MapSettingDefaultResetDto.EdgeDirectionAllowed = dto.EdgeDirectionAllowed; + MapSettingDefaultResetDto.EdgeRotationAllowed = dto.EdgeRotationAllowed; + MapSettingDefaultResetDto.EdgeAllowedDeviationXy = dto.EdgeAllowedDeviationXy; + MapSettingDefaultResetDto.EdgeAllowedDeviationTheta = dto.EdgeAllowedDeviationTheta; + + MapSettingDefaultResetDto.NodeAllowedDeviationXy = dto.NodeAllowedDeviationXy; + MapSettingDefaultResetDto.NodeAllowedDeviationTheta = dto.NodeAllowedDeviationTheta; + MapSettingDefaultResetDto.NodeNameAutoGenerate = dto.NodeNameAutoGenerate; + MapSettingDefaultResetDto.NodeNameTemplate = dto.NodeNameTemplate; + + MapSettingDefaultResetDto.ZoneMinSquare = dto.ZoneMinSquare; + } + + private void SetConfigBindingDto(MapSettingDefaultDto dto) + { + MapSettingDefaultDto.Id = dto.Id; + + MapSettingDefaultDto.EdgeMinLength = dto.EdgeMinLength; + MapSettingDefaultDto.EdgeMinHeight = dto.EdgeMinHeight; + MapSettingDefaultDto.EdgeMaxHeight = dto.EdgeMaxHeight; + MapSettingDefaultDto.EdgeMaxRotationSpeed = dto.EdgeMaxRotationSpeed; + MapSettingDefaultDto.EdgeStraightMaxSpeed = dto.EdgeStraightMaxSpeed; + MapSettingDefaultDto.EdgeCurveMaxSpeed = dto.EdgeCurveMaxSpeed; + MapSettingDefaultDto.EdgeDirectionAllowed = dto.EdgeDirectionAllowed; + MapSettingDefaultDto.EdgeRotationAllowed = dto.EdgeRotationAllowed; + MapSettingDefaultDto.EdgeAllowedDeviationXy = dto.EdgeAllowedDeviationXy; + MapSettingDefaultDto.EdgeAllowedDeviationTheta = dto.EdgeAllowedDeviationTheta; + + MapSettingDefaultDto.NodeAllowedDeviationXy = dto.NodeAllowedDeviationXy; + MapSettingDefaultDto.NodeAllowedDeviationTheta = dto.NodeAllowedDeviationTheta; + MapSettingDefaultDto.NodeNameAutoGenerate = dto.NodeNameAutoGenerate; + MapSettingDefaultDto.NodeNameTemplate = dto.NodeNameTemplate; + + MapSettingDefaultDto.ZoneMinSquare = dto.ZoneMinSquare; + } + + public void Reset() + { + SetConfigBindingDto(MapSettingDefaultResetDto); + StateHasChanged(); + } + + public async Task Save() + { + if (!ValidateUpdateData(MapSettingDefaultResetDto, MapSettingDefaultDto)) + { + Snackbar.Add("Không có sự thay đổi", Severity.Warning); + return; + } + + var result = await (await Http.PutAsJsonAsync("api/MapsSetting", MapSettingDefaultDto)).Content.ReadFromJsonAsync(); + + if (result == null) + { + Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + return; + } + else if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + return; + } + + SetConfigResetDto(MapSettingDefaultDto); + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + + private bool ValidateUpdateData(MapSettingDefaultDto old, MapSettingDefaultDto update) + { + if (old.EdgeStraightMaxSpeed != update.EdgeStraightMaxSpeed) return true; + if (old.EdgeCurveMaxSpeed != update.EdgeCurveMaxSpeed) return true; + if (old.EdgeMaxRotationSpeed != update.EdgeMaxRotationSpeed) return true; + if (old.EdgeMinLength != update.EdgeMinLength) return true; + if (old.EdgeMinHeight != update.EdgeMinHeight) return true; + if (old.EdgeMaxHeight != update.EdgeMaxHeight) return true; + if (old.EdgeDirectionAllowed != update.EdgeDirectionAllowed) return true; + if (old.EdgeRotationAllowed != update.EdgeRotationAllowed) return true; + if (old.EdgeAllowedDeviationXy != update.EdgeAllowedDeviationXy) return true; + if (old.EdgeAllowedDeviationTheta != update.EdgeAllowedDeviationTheta) return true; + + if (old.NodeAllowedDeviationXy != update.NodeAllowedDeviationXy) return true; + if (old.NodeAllowedDeviationTheta != update.NodeAllowedDeviationTheta) return true; + if (old.NodeNameAutoGenerate != update.NodeNameAutoGenerate) return true; + if (old.NodeNameTemplate != update.NodeNameTemplate) return true; + + if (old.ZoneMinSquare != update.ZoneMinSquare) return true; + return false; + } +} diff --git a/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor.css b/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor.css new file mode 100644 index 0000000..c330548 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Setting/MapSettingDefault.razor.css @@ -0,0 +1,6 @@ +.config-item { + display: flex; + flex-direction: row; + margin-left: 2rem; + margin-top: 1rem; +} diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor b/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor new file mode 100644 index 0000000..2ca4eb8 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor @@ -0,0 +1,81 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +@code { + [Parameter] + public EventCallback EditorStateChanged { get; set; } + + [Parameter] + public EventCallback AlignStateClick { get; set; } + + [CascadingParameter] + public bool MapIsActive { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + [Parameter] + public bool MultiSelectedNode { get; set; } + + [Parameter] + public bool MultiSelectedEdge { get; set; } + + private bool alignDisable => MapIsActive || EditorState != EditorState.Scaner || !MultiSelectedNode; + private bool moveDisable => MapIsActive || (EditorState != EditorState.Scaner && EditorState != EditorState.Move) || !MultiSelectedNode; + private bool copyDisable => MapIsActive || (EditorState != EditorState.Scaner && EditorState != EditorState.Copy) || !MultiSelectedEdge; + + private async Task EditButtonChanged(EditorState state) => await EditorStateChanged.InvokeAsync(state); + + private async Task AlignButtonCick(AlignState state) => await AlignStateClick.InvokeAsync(state); +} diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor.css b/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor.css new file mode 100644 index 0000000..961ccff --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/AlignmentToolbar.razor.css @@ -0,0 +1,19 @@ +.align-toolbar { + display: flex; + flex-direction: row; + align-items: center; + margin: 0 10px; + padding: 0 5px 0 10px; + border-left: 1px solid gray; + border-right: 1px solid gray; + margin-top: 1px +} + +.align-button { + padding: 1px 0.4rem; + margin-right: 5px; +} + +.icon-button { + font-size: 1.6rem; +} diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor b/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor new file mode 100644 index 0000000..a84f225 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor @@ -0,0 +1,209 @@ +@inject IJSRuntime JSRuntime +@inject NavigationManager Nav + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + + + + + + + + + Edge Width + + + Edge Direction Width + + + Node Radius + + + Origin Width + + + + + + + + + + + + + + + +
+
+ + + + + + + + + +
+
+
+ +@code { + [Parameter] + public EventCallback ControlStateClick { get; set; } + + [Parameter] + public EventCallback<(ControlState, bool)> EditorExtensionChanged { get; set; } + + [CascadingParameter] + public bool MapIsActive { get; set; } + + [Parameter] + public bool ShowGrid { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public bool ShowMapSlam { get; set; } + + [Parameter] + public bool NodesUndoable { get; set; } + + [Parameter] + public bool MultiSelectedEdge { get; set; } + + [Parameter] + public bool ZoneSelected { get; set; } + + private double EdgeWidth = 0.15; + private double EdgeDirectionWidth = 0.3; + private double NodeRadius = 0.1; + private double OriginVectorWidth = 0.35; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + var width = await JSRuntime.InvokeAsync("getCssVariable", "--edge-stroke-width"); + if (!string.IsNullOrEmpty(width)) + { + if (double.TryParse(width.Replace("px", ""), out double edgewidth)) + { + EdgeWidth = edgewidth; + } + } + else await JSRuntime.InvokeVoidAsync("setCssVariable", "--edge-stroke-width", $"{EdgeWidth}px"); + + var directionwidth = await JSRuntime.InvokeAsync("getCssVariable", "--edge-direction-stroke-width"); + if (!string.IsNullOrEmpty(directionwidth)) + { + if (double.TryParse(directionwidth.Replace("px", ""), out double edgedirectionwidth)) + { + EdgeDirectionWidth = edgedirectionwidth; + } + } + else await JSRuntime.InvokeVoidAsync("setCssVariable", "--edge-direction-stroke-width", $"{EdgeDirectionWidth}px"); + + var radius = await JSRuntime.InvokeAsync("getCssVariable", "--node-r"); + if (!string.IsNullOrEmpty(radius)) + { + if (double.TryParse(radius.Replace("px", ""), out double noderadius)) + { + NodeRadius = noderadius; + } + } + else await JSRuntime.InvokeVoidAsync("setCssVariable", "--node-r", $"{NodeRadius}px"); + + var originwidth = await JSRuntime.InvokeAsync("getCssVariable", "--origin-vector-stroke-width"); + if (!string.IsNullOrEmpty(originwidth)) + { + if (double.TryParse(originwidth.Replace("px", ""), out double originVectorWidth)) + { + OriginVectorWidth = originVectorWidth; + } + } + else await JSRuntime.InvokeVoidAsync("setCssVariable", "--origin-vector-stroke-width", $"{OriginVectorWidth}px"); + StateHasChanged(); + } + + private async Task ShowNameChanged() => await EditorExtensionChanged.InvokeAsync((ControlState.ShowName, ShowName)); + + private async Task ShowGridChanged() => await EditorExtensionChanged.InvokeAsync((ControlState.ShowGrid, ShowGrid)); + + private async Task ShowSlamMapChanged() => await EditorExtensionChanged.InvokeAsync((ControlState.ShowMapSlam, ShowMapSlam)); + + private async Task EdgeWidthChanged(double value) + { + EdgeWidth = value; + var width = $"{EdgeWidth}px"; + await JSRuntime.InvokeVoidAsync("setCssVariable", "--edge-stroke-width", width); + } + + private async Task EdgeDirectionWidthChanged(double value) + { + EdgeDirectionWidth = value; + var width = $"{EdgeDirectionWidth}px"; + await JSRuntime.InvokeVoidAsync("setCssVariable", "--edge-direction-stroke-width", width); + } + + private async Task NodeRadiusChanged(double value) + { + NodeRadius = value; + var width = $"{NodeRadius}px"; + await JSRuntime.InvokeVoidAsync("setCssVariable", "--node-r", width); + } + + private async Task OriginVectorWidthChanged(double value) + { + OriginVectorWidth = value; + var width = $"{OriginVectorWidth}px"; + await JSRuntime.InvokeVoidAsync("setCssVariable", "--origin-vector-stroke-width", width); + } + +} diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor.css b/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor.css new file mode 100644 index 0000000..634be7a --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/ControlToolbar.razor.css @@ -0,0 +1,63 @@ +.control-toolbar { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 1px; + flex-grow: 1; +} + + .control-toolbar .check-box { + display: flex; + flex-direction: row; + align-items: center; + } + + .control-toolbar .control-button { + display: flex; + flex-direction: row-reverse; + flex-grow: 1; + } + + .control-toolbar .control-button .extension { + display: flex; + flex-direction: row-reverse; + padding-left: 10px; + } + + .control-toolbar .control-button .scale-map { + display: flex; + flex-direction: row-reverse; + flex-grow: 1; + padding-right: 5px; + padding-left: 10px; + border-left: 1px solid gray; + border-right: 1px solid gray; + } + +.button { + padding: 1px 0.4rem; + margin-right: 5px; +} + +.icon { + font-size: 1.6rem; +} + +.button-dropdown { + padding: 1px 0.3rem 1px 0.5rem; + margin-right: 5px; +} + +.dropdown-toggle::after { + border: 0px; +} + +.custom-range { + --progress: 0%; +} + + .custom-range::-webkit-slider-runnable-track { + height: 8px; + border-radius: 5px; + background: linear-gradient(to right, #0d6efd 0%, #0d6efd var(--progress), #e9ecef 0%); + } \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor b/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor new file mode 100644 index 0000000..12da73a --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor @@ -0,0 +1,106 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Operating + OperatingHazard + Restricted + LoadTransfer + Confined + Forbidden + +
+
+ +@code { + [Parameter] + public EventCallback EditorStateChanged { get; set; } + + [Parameter] + public EventCallback EditorZoneTypeChanged { get; set; } + + [CascadingParameter] + public bool MapIsActive { get; set; } + + private EditorState State = EditorState.View; + private ZoneType Type = ZoneType.Operating; + + private async Task EditorButtonChanged(EditorState state) + { + if (state != State) + { + State = state; + await EditorStateChanged.InvokeAsync(state); + } + } + + private async Task ZoneTypeButtonChanged() + { + await EditorZoneTypeChanged.InvokeAsync(Type); + } + + public void SetEditorState(EditorState state) + { + if (State != state) + { + State = state; + StateHasChanged(); + } + } +} diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor.css b/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor.css new file mode 100644 index 0000000..5e3f7cf --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/EditorFunctionToolbar.razor.css @@ -0,0 +1,16 @@ +.edit-toolbar { + display: flex; + flex-direction: row; + align-items: center; +} + +.icon-button { + font-size: 1.7rem; + padding: 0.4rem; +} + +.zone-select { + margin-left: 10px; + margin-top: 1px; + align-content: center; +} \ No newline at end of file diff --git a/RobotNet.WebApp/Maps/Components/Toolbar/EditorToolbar.razor b/RobotNet.WebApp/Maps/Components/Toolbar/EditorToolbar.razor new file mode 100644 index 0000000..fb3acd4 --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/Toolbar/EditorToolbar.razor @@ -0,0 +1,62 @@ +
+ + + + + +
+ +@code { + [Parameter] + public bool MapIsActive { get; set; } + + [Parameter] + public EventCallback EditorStateChanged { get; set; } + + [Parameter] + public EventCallback EditorZoneTypeChanged { get; set; } + + [Parameter] + public EditorState EditorState { get; set; } + + [Parameter] + public bool MultiSelectedNode { get; set; } + + [Parameter] + public bool MultiSelectedEdge { get; set; } + + [Parameter] + public bool ZoneSelected { get; set; } + + [Parameter] + public EventCallback AlignStateClick { get; set; } + + [Parameter] + public EventCallback ControlStateClick { get; set; } + + [Parameter] + public EventCallback<(ControlState, bool)> EditorExtensionChanged { get; set; } + + [Parameter] + public bool ShowGrid { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public bool ShowMapSlam { get; set; } + + [Parameter] + public bool NodesUndoable { get; set; } + + private EditorFunctionToolbar EditorFunctionToolbarRef = null!; + + public void SetEditorState(EditorState state) => EditorFunctionToolbarRef.SetEditorState(state); +} diff --git a/RobotNet.WebApp/Maps/Components/_Imports.razor b/RobotNet.WebApp/Maps/Components/_Imports.razor new file mode 100644 index 0000000..62447eb --- /dev/null +++ b/RobotNet.WebApp/Maps/Components/_Imports.razor @@ -0,0 +1,8 @@ +@using RobotNet.MapShares +@using RobotNet.MapShares.Dtos +@using RobotNet.MapShares.Enums +@using RobotNet.MapShares.Property +@using RobotNet.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Maps.Models +@using System.Text.Json diff --git a/RobotNet.WebApp/Maps/Models/EdgeModel.cs b/RobotNet.WebApp/Maps/Models/EdgeModel.cs new file mode 100644 index 0000000..b939811 --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/EdgeModel.cs @@ -0,0 +1,131 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; +using System.Text.Json; + +namespace RobotNet.WebApp.Maps.Models; + +public class EdgeModel : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public double X1 => StartNode.X; + public double Y1 => StartNode.Y; + public double X2 => EndNode.X; + public double Y2 => EndNode.Y; + public DirectionAllowed DirectionAllowed => Data.DirectionAllowed; + public TrajectoryDegree TrajectoryDegree => Data.TrajectoryDegree; + public double ControlPoint1X => Data.ControlPoint1X; + public double ControlPoint1Y => Data.ControlPoint1Y; + public double ControlPoint2X => Data.ControlPoint2X; + public double ControlPoint2Y => Data.ControlPoint2Y; + public double MaxSpeed => Data.MaxSpeed; + public double MaxHeight => Data.MaxHeight; + public double MinHeight => Data.MinHeight; + public bool RotationAllowed => Data.RotationAllowed; + public double MaxRotationSpeed => Data.MaxRotationSpeed; + public string Actions => Data.Actions; + public double AllowedDeviationXy => Data.AllowedDeviationXy; + public double AllowedDeviationTheta => Data.AllowedDeviationTheta; + + public bool ActivedControlPoint1 { get; set; } + public bool ActivedControlPoint2 { get; set; } + + public NodeModel StartNode { get; private set; } + public NodeModel EndNode { get; private set; } + + public event Func? StartNodePositionChanged; + public event Func? EndNodePositionChanged; + public event Func? ControlPoint1PositionChanged; + public event Func? ControlPoint2PositionChanged; + public event Func? DirectionChanged; + + public event Action? ActiveChanged; + public event Action? SelectedChanged; + public event Action? ErrorChanged; + + private readonly EdgeDto Data; + public EdgeModel(EdgeDto data, NodeModel startNode, NodeModel endNode) + { + Data = data; + StartNode = startNode; + EndNode = endNode; + + StartNode.PositionChanged += StartNode_PositionChanged; + EndNode.PositionChanged += EndNode_PositionChanged; + } + + private void EndNode_PositionChanged() => EndNodePositionChanged?.Invoke(); + private void StartNode_PositionChanged() => StartNodePositionChanged?.Invoke(); + public void Active() => ActiveChanged?.Invoke(true); + public void UnActive() => ActiveChanged?.Invoke(false); + public void Selected() => SelectedChanged?.Invoke(this); + + public void UpdateNode(Guid id, NodeModel node) + { + if (StartNode.Id == id) + { + StartNode.PositionChanged -= StartNode_PositionChanged; + StartNode = node; + StartNode.PositionChanged += StartNode_PositionChanged; + StartNodePositionChanged?.Invoke(); + } + else if (EndNode.Id == id) + { + EndNode.PositionChanged -= EndNode_PositionChanged; + EndNode = node; + EndNode.PositionChanged += EndNode_PositionChanged; + EndNodePositionChanged?.Invoke(); + } + } + + public void UpdateData(EdgeUpdateModel model) + { + if(Data.ControlPoint1X != model.ControlPoint1X || Data.ControlPoint1Y != model.ControlPoint1Y) + { + Data.ControlPoint1X = model.ControlPoint1X; + Data.ControlPoint1Y = model.ControlPoint1Y; + ControlPoint1PositionChanged?.Invoke(); + } + if (Data.ControlPoint2X != model.ControlPoint2X || Data.ControlPoint2Y != model.ControlPoint2Y) + { + Data.ControlPoint2X = model.ControlPoint2X; + Data.ControlPoint2Y = model.ControlPoint2Y; + ControlPoint2PositionChanged?.Invoke(); + } + Data.RotationAllowed = model.RotationAllowed; + Data.MaxSpeed = model.MaxSpeed; + Data.MaxRotationSpeed = model.MaxRotationSpeed; + Data.MaxHeight = model.MaxHeight; + Data.MinHeight = model.MinHeight; + Data.Actions = JsonSerializer.Serialize(model.Actions); + + if (Data.DirectionAllowed != model.DirectionAllowed) + { + Data.DirectionAllowed = model.DirectionAllowed; + DirectionChanged?.Invoke(); + } + } + + public void UpdateControlPoint1(double x, double y) + { + Data.ControlPoint1X = x; + Data.ControlPoint1Y = y; + ControlPoint1PositionChanged?.Invoke(); + } + + public void UpdateControlPoint2(double x, double y) + { + Data.ControlPoint2X = x; + Data.ControlPoint2Y = y; + ControlPoint2PositionChanged?.Invoke(); + } + + public void SetError(bool state) => ErrorChanged?.Invoke(state); + + public void Dispose() + { + StartNode.PositionChanged -= StartNode_PositionChanged; + EndNode.PositionChanged -= EndNode_PositionChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Models/ElementModel.cs b/RobotNet.WebApp/Maps/Models/ElementModel.cs new file mode 100644 index 0000000..157cc99 --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/ElementModel.cs @@ -0,0 +1,60 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.WebApp.Maps.Models; + +#nullable disable + +public class ElementModel : IDisposable +{ + public Guid Id => Element.Id; + public Guid ModelId => Element.ModelId; + public Guid NodeId => Element.NodeId; + public Guid MapId => Element.MapId; + public string Name => Element.Name; + public double X => Node.X; + public double Y => Node.Y; + public double Theta => Node.Theta; + public double OffsetX => Element.OffsetX; + public double OffsetY => Element.OffsetY; + public bool IsOpen => Element.IsOpen; + public string Content => Element.Content; + + public event Func PositionChanged; + public event Func OffsetChanged; + + public ElementDto Element { get; private set; } + public NodeModel Node { get; private set; } + + public ElementModel(ElementDto element, NodeModel node) + { + Element = element; + Node = node; + + Node.PositionChanged += Node_PositionChanged; + } + + public void Node_PositionChanged() => PositionChanged?.Invoke(); + + public void UpdateOffset(double offsetX, double offsetY) + { + if (Element.OffsetX == offsetX && Element.OffsetY == offsetY) return; + + Element.OffsetX = offsetX; + Element.OffsetY = offsetY; + OffsetChanged?.Invoke(); + } + + public void UpdateElement(ElementDto model) + { + Element.Name = model.Name; + Element.IsOpen = model.IsOpen; + Element.Content = model.Content; + UpdateOffset(model.OffsetX, model.OffsetY); + } + + public void Dispose() + { + Node.PositionChanged -= Node_PositionChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Models/MapEdgeModel.cs b/RobotNet.WebApp/Maps/Models/MapEdgeModel.cs new file mode 100644 index 0000000..53aaf43 --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/MapEdgeModel.cs @@ -0,0 +1,123 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.WebApp.Maps.Models; + +public class MapEdgeModel : IEnumerable +{ + private readonly Dictionary dicEdges = []; + public EdgeModel this[int i] => dicEdges.Values.ElementAt(i); + public EdgeModel this[Guid id] => dicEdges[id]; + public IEnumerator GetEnumerator() => dicEdges.Values.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + public bool TryGetValue(Guid id, out EdgeModel? value) => dicEdges.TryGetValue(id, out value); + public bool ContainsKey(Guid key) => dicEdges.ContainsKey(key); + + public List ActivedEdges { get; private set; } = []; + + public event Action? Changed; + public event Action? EdgeSelectedChanged; + private (double X, double Y) StartMovePosition; + + public void Add(EdgeDto edge, NodeModel start, NodeModel end) + { + if (dicEdges.ContainsKey(edge.Id)) return; + var model = new EdgeModel(edge, start, end); + dicEdges.Add(model.Id, model); + model.SelectedChanged += EdgeSelectedChange; + Changed?.Invoke(); + } + + public void ReplaceAll(IEnumerable edges) + { + foreach (var edge in dicEdges.Values) + { + if (ActivedEdges.Any(e => e.Id == edge.Id)) + { + edge.SelectedChanged -= EdgeSelectedChange; + edge.UnActive(); + ActivedEdges.Remove(edge); + } + edge.Dispose(); + } + dicEdges.Clear(); + + foreach (var edge in edges) + { + dicEdges.Add(edge.Id, edge); + edge.SelectedChanged += EdgeSelectedChange; + } + Changed?.Invoke(); + } + + public void Remove(EdgeModel edge) + { + if (!dicEdges.ContainsKey(edge.Id)) return; + + edge.SelectedChanged -= EdgeSelectedChange; + if (ActivedEdges.Any(e => e.Id == edge.Id)) + { + ActivedEdges.Remove(edge); + } + edge.Dispose(); + + dicEdges.Remove(edge.Id); + Changed?.Invoke(); + } + + private void EdgeSelectedChange(EdgeModel edge) + { + EdgeSelectedChanged?.Invoke(edge); + } + + public void UnActivedEdge(EdgeModel? exceptedModel = null) + { + foreach (var edge in ActivedEdges) + { + if (exceptedModel is not null && edge.Id == exceptedModel.Id) continue; + var edgeInDic = dicEdges.Values.FirstOrDefault(e => e.Id.Equals(edge.Id)); + edgeInDic?.UnActive(); + } + ActivedEdges.Clear(); + if (exceptedModel is not null) ActivedEdges.Add(exceptedModel); + } + + public void ActivedEdge(IEnumerable edges) + { + UnActivedEdge(); + foreach (var edge in edges) + { + var edgeInDic = dicEdges[edge.Id]; + edgeInDic?.Active(); + ActivedEdges.Add(edge); + } + } + + public void SetStartMovePosition(double x, double y) + { + StartMovePosition.X = x; + StartMovePosition.Y = y; + } + + public void UpdateMoveEdge(double x, double y) + { + double deltaX = x - StartMovePosition.X; + double deltaY = y - StartMovePosition.Y; + List NodeUpdated = []; + foreach (var edge in ActivedEdges) + { + if (!NodeUpdated.Any(n => n == edge.StartNode.Id)) edge.StartNode.UpdatePosition(edge.StartNode.X + deltaX, edge.StartNode.Y + deltaY); + if (!NodeUpdated.Any(n => n == edge.EndNode.Id)) edge.EndNode.UpdatePosition(edge.EndNode.X + deltaX, edge.EndNode.Y + deltaY); + if (edge.TrajectoryDegree == TrajectoryDegree.Two) edge.UpdateControlPoint1(edge.ControlPoint1X + deltaX, edge.ControlPoint1Y + deltaY); + if (edge.TrajectoryDegree == TrajectoryDegree.Three) + { + edge.UpdateControlPoint1(edge.ControlPoint1X + deltaX, edge.ControlPoint1Y + deltaY); + edge.UpdateControlPoint2(edge.ControlPoint2X + deltaX, edge.ControlPoint2Y + deltaY); + } + NodeUpdated.Add(edge.StartNode.Id); + NodeUpdated.Add(edge.EndNode.Id); + } + StartMovePosition.X = x; + StartMovePosition.Y = y; + } +} diff --git a/RobotNet.WebApp/Maps/Models/MapElementModel.cs b/RobotNet.WebApp/Maps/Models/MapElementModel.cs new file mode 100644 index 0000000..2357478 --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/MapElementModel.cs @@ -0,0 +1,45 @@ +using System.Collections; + +namespace RobotNet.WebApp.Maps.Models; + +public class MapElementModel : IEnumerable +{ + private readonly Dictionary dicElements = []; + public IEnumerator GetEnumerator() => dicElements.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public bool TryGetValue(Guid id, out ElementModel? value) => dicElements.TryGetValue(id, out value); + public bool ContainsKey(Guid key) => dicElements.ContainsKey(key); + + public event Action? Changed; + + public void Add(ElementModel element) + { + if (dicElements.ContainsKey(element.Id)) return; + + dicElements.Add(element.Id, element); + Changed?.Invoke(); + } + + public void ReplaceAll(IEnumerable elements) + { + foreach (var robot in dicElements.Values) + { + robot.Dispose(); + } + dicElements.Clear(); + + foreach (var element in elements) + { + dicElements.Add(element.Id, element); + } + Changed?.Invoke(); + } + + public void Remove(ElementModel element) + { + if (!dicElements.ContainsKey(element.Id)) return; + element.Dispose(); + dicElements.Remove(element.Id); + Changed?.Invoke(); + } +} diff --git a/RobotNet.WebApp/Maps/Models/MapNodeModel.cs b/RobotNet.WebApp/Maps/Models/MapNodeModel.cs new file mode 100644 index 0000000..0e626c3 --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/MapNodeModel.cs @@ -0,0 +1,86 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.WebApp.Maps.Models; + +public class MapNodeModel : IEnumerable +{ + private readonly Dictionary dicNodes = []; + public NodeModel this[int i] => dicNodes.Values.ElementAt(i); + public NodeModel this[Guid id] => dicNodes[id]; + public IEnumerator GetEnumerator() => dicNodes.Values.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + public bool ContainsKey(Guid key) => dicNodes.ContainsKey(key); + public bool TryGetValue(Guid id, out NodeModel? value) => dicNodes.TryGetValue(id, out value); + + public NodeModel? SelectedNode { get; set; } + public List ActivedNodes { get; private set; } = []; + + public event Action? Changed; + + private (double X, double Y) StartMovePosition; + + public void Add(NodeDto node) + { + if (dicNodes.ContainsKey(node.Id)) return; + + var model = new NodeModel(node); + dicNodes.Add(model.Id, model); + Changed?.Invoke(); + } + + public void ReplaceAll(IEnumerable nodes) + { + dicNodes.Clear(); + + foreach (var node in nodes) + { + var model = new NodeModel(node); + dicNodes.Add(model.Id, model); + } + Changed?.Invoke(); + } + + public void Remove(NodeModel node) + { + if (!dicNodes.ContainsKey(node.Id)) return; + + node.Dispose(); + dicNodes.Remove(node.Id); + Changed?.Invoke(); + } + + public void ActivedNode(IEnumerable nodes) + { + UnActivedNode(); + foreach (var node in nodes) + { + var nodeInDic = dicNodes.Values.FirstOrDefault(e => e.Id.Equals(node.Id)); + nodeInDic?.Active(); + ActivedNodes.Add(node); + } + } + public void UnActivedNode() + { + foreach (var node in ActivedNodes) + { + var nodeInDic = dicNodes.Values.FirstOrDefault(e => e.Id.Equals(node.Id)); + nodeInDic?.UnActive(); + } + ActivedNodes.Clear(); + } + + public void SetStartMovePosition(double x, double y) + { + StartMovePosition.X = x; + StartMovePosition.Y = y; + } + + public void UpdateMoveNode(double x, double y) + { + double deltaX = x - StartMovePosition.X; + double deltaY = y - StartMovePosition.Y; + ActivedNodes.ForEach(n => n.UpdatePosition(n.X + deltaX, n.Y + deltaY)); + StartMovePosition.X = x; + StartMovePosition.Y = y; + } +} diff --git a/RobotNet.WebApp/Maps/Models/MapZoneModel.cs b/RobotNet.WebApp/Maps/Models/MapZoneModel.cs new file mode 100644 index 0000000..ff10b4c --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/MapZoneModel.cs @@ -0,0 +1,73 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.WebApp.Maps.Models; + +public class MapZoneModel : IEnumerable +{ + private readonly Dictionary dicZones = []; + public ZoneModel this[int i] => dicZones.Values.ElementAt(i); + public ZoneModel this[Guid id] => dicZones[id]; + public IEnumerator GetEnumerator() => dicZones.Values.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + public bool TryGetValue(Guid id, out ZoneModel? value) => dicZones.TryGetValue(id, out value); + + + public ZoneModel? ZoneActived { get; private set; } + public event Action? Changed; + public event Action? ActivedZoneChanged; + + public void ReplaceAll(IEnumerable zones) + { + foreach (var zone in dicZones.Values) + { + zone.ActiveChanged -= Zone_ActiveChanged; + zone.Dispose(); + if (ZoneActived is not null && ZoneActived == zone) ZoneActived = null; + } + dicZones.Clear(); + + foreach (var zone in zones) + { + var model = new ZoneModel(zone); + model.ActiveChanged += Zone_ActiveChanged; + dicZones.Add(zone.Id, model); + } + Changed?.Invoke(); + } + + public void Add(ZoneDto zone) + { + if (dicZones.ContainsKey(zone.Id)) return; + + var model = new ZoneModel(zone); + dicZones.Add(model.Id, model); + model.ActiveChanged += Zone_ActiveChanged; + Changed?.Invoke(); + } + + public void Remove(ZoneModel zone) + { + if (!dicZones.ContainsKey(zone.Id)) return; + + zone.UnActive(); + zone.ActiveChanged -= Zone_ActiveChanged; + if (ZoneActived is not null && zone.Id == ZoneActived.Id) ZoneActived = null; + zone.Dispose(); + dicZones.Remove(zone.Id); + Changed?.Invoke(); + } + + private void Zone_ActiveChanged(ZoneModel zone, bool state) + { + if (state) + { + if (ZoneActived is not null && ZoneActived != zone || ZoneActived is null) + { + ZoneActived?.UnActive(); + ZoneActived = zone; + } + } + ActivedZoneChanged?.Invoke(zone, state); + } + public void UnActivedZone() => ZoneActived?.UnActive(); +} diff --git a/RobotNet.WebApp/Maps/Models/NodeModel.cs b/RobotNet.WebApp/Maps/Models/NodeModel.cs new file mode 100644 index 0000000..599387e --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/NodeModel.cs @@ -0,0 +1,59 @@ +using RobotNet.MapShares.Dtos; +using System.Text.Json; + +namespace RobotNet.WebApp.Maps.Models; + +public class NodeModel(NodeDto Data) : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public string Name => Data.Name; + public double X => Data.X; + public double Y => Data.Y; + public double Theta => Data.Theta; + public string Actions => Data.Actions; + public double AllowedDeviationXy => Data.AllowedDeviationXy; + public double AllowedDeviationTheta => Data.AllowedDeviationTheta; + + public event Action? PositionChanged; + public event Action? ActivedChanged; + public event Action? ErrorChanged; + + public int NumberOfEdgeReference => PositionChanged?.GetInvocationList().Length ?? 0; + + public void UpdatePosition(double x, double y) + { + if (Data.X == x && Data.Y == y) return; + + Data.X = x; + Data.Y = y; + PositionChanged?.Invoke(); + } + public void UpdatePosition(double x, double y, double theta) + { + if (Data.X == x && Data.Y == y && Data.Theta == theta) return; + + Data.X = x; + Data.Y = y; + Data.Theta = theta; + PositionChanged?.Invoke(); + } + + public void UpdateData(NodeUpdateModel model) + { + Data.Name = model.Name; + Data.AllowedDeviationXy = model.AllowedDeviationXy; + Data.AllowedDeviationTheta = model.AllowedDeviationTheta; + Data.Actions = JsonSerializer.Serialize(model.Actions); + UpdatePosition(model.X, model.Y, model.Theta); + } + + public void Active() => ActivedChanged?.Invoke(true); + public void UnActive() => ActivedChanged?.Invoke(false); + public void SetError(bool state) => ErrorChanged?.Invoke(state); + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Maps/Models/ZoneModel.cs b/RobotNet.WebApp/Maps/Models/ZoneModel.cs new file mode 100644 index 0000000..026711a --- /dev/null +++ b/RobotNet.WebApp/Maps/Models/ZoneModel.cs @@ -0,0 +1,121 @@ +using RobotNet.MapShares; +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.WebApp.Maps.Models; + +public class ZoneModel(ZoneDto Data) : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public ZoneType Type => Data.Type; + public string Name => Data.Name; + + public double X1 => Data.X1; + public double Y1 => Data.Y1; + public double X2 => Data.X2; + public double Y2 => Data.Y2; + public double X3 => Data.X3; + public double Y3 => Data.Y3; + public double X4 => Data.X4; + public double Y4 => Data.Y4; + + public string Fill => Data.Type switch + { + ZoneType.Confined => "#c29ffa", // Vùng hoạt động hạn chế + ZoneType.Forbidden => "#ea868f", // Vùng cấm di chuyển + ZoneType.Operating => "#79dfc1", // Vùng hoạt động bình thường + ZoneType.OperatingHazard => "#ffda6a", // Vùng hoạt động nguy hiểm + ZoneType.Restricted => "#fd9843", // Vùng hoạt động giới hạn + ZoneType.LoadTransfer => "#e685b5", // Vùng chuyển hàng + _ => "none", + }; + + public int ActiveNode = -1; + public event Action? ActiveChanged; + public event Func? ControlNodePosittionChanged; + public event Action? TypeChanged; + + private (double X, double Y) StartMovePosition; + public void Active() + { + if (ActiveNode == -1) ActiveNode = 5; + ActiveChanged?.Invoke(this, true); + } + + public void UnActive() => ActiveChanged?.Invoke(this, false); + + public void UpdateControlNode(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) + { + Data.X1 = x1; + Data.Y1 = y1; + Data.X2 = x2; + Data.Y2 = y2; + Data.X3 = x3; + Data.Y3 = y3; + Data.X4 = x4; + Data.Y4 = y4; + ControlNodePosittionChanged?.Invoke(); + } + + public void UpdateData(ZoneUpdateModel data) + { + if (data.Type != Data.Type) + { + Data.Type = data.Type; + TypeChanged?.Invoke(); + } + if (data.Name != Data.Name) Data.Name = data.Name; + UpdateControlNode(data.X1, data.Y1, data.X2, data.Y2, data.X3, data.Y3, data.X4, data.Y4); + } + + public void SetStartMovePosition(double x, double y) + { + StartMovePosition.X = x; + StartMovePosition.Y = y; + } + + public void UpdateControlNode(int node, double x, double y) + { + double deltaX = x - StartMovePosition.X; + double deltaY = y - StartMovePosition.Y; + switch (node) + { + case 1: + Data.X1 = x; + Data.Y1 = y; + break; + case 2: + Data.X2 = x; + Data.Y2 = y; + break; + case 3: + Data.X3 = x; + Data.Y3 = y; + break; + case 4: + Data.X4 = x; + Data.Y4 = y; + break; + case 5: + if (!MapEditorHelper.IsPointInside(StartMovePosition.X, StartMovePosition.Y, Data)) return; + Data.X1 += deltaX; + Data.Y1 += deltaY; + Data.X2 += deltaX; + Data.Y2 += deltaY; + Data.X3 += deltaX; + Data.Y3 += deltaY; + Data.X4 += deltaX; + Data.Y4 += deltaY; + break; + } + StartMovePosition.X = x; + StartMovePosition.Y = y; + ControlNodePosittionChanged?.Invoke(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Pages/Authentication.razor b/RobotNet.WebApp/Pages/Authentication.razor new file mode 100644 index 0000000..6c74356 --- /dev/null +++ b/RobotNet.WebApp/Pages/Authentication.razor @@ -0,0 +1,7 @@ +@page "/authentication/{action}" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + + +@code{ + [Parameter] public string? Action { get; set; } +} diff --git a/RobotNet.WebApp/Pages/Dashboard.razor b/RobotNet.WebApp/Pages/Dashboard.razor new file mode 100644 index 0000000..d9b836e --- /dev/null +++ b/RobotNet.WebApp/Pages/Dashboard.razor @@ -0,0 +1,105 @@ +@page "/" +@attribute [Authorize] + +@using RobotNet.Script.Shares.Dashboard +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Dashboard.Components + +@inject IJSRuntime JSRuntime +@inject NavigationManager Nav +@inject DashboardHubClient DashboardHub +@inject ISnackbar Snackbar + +Dashboard + +
+
+ AMR MONITORING +
+ @timeUpdate +
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+ +@code { + private bool isFullScreen = false; + + private DailyData DailyDataRef = default!; + private PerformancePieChart TodayPerformancePieChartRef = default!; + private PerformancePieChart WeekyPerformancePieChartRef = default!; + private MissionsPerformanceBarChart MissionsPerformanceBarChartRef = default!; + private TaktTimeLineChart TaktTimeLineChartRef = default!; + + private string timeUpdate = ""; + + private async Task ToogleFullScreen() + { + await JSRuntime.InvokeVoidAsync("setFullscreen", isFullScreen ? false : true); + isFullScreen = isFullScreen ? false : true; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + DashboardHub.DashboardDataUpdated += (async (value) => await DashboardHub_DashboardDataUpdated(value)); + await DashboardHub.StartAsync(); + await GetDashboardData(); + } + + private async Task GetDashboardData() + { + var dashboardData = await DashboardHub.GetDashboardData(); + if (dashboardData.IsSuccess && dashboardData.Data is not null) + { + timeUpdate = dashboardData.Data.TimeUpdate; + DailyDataRef.UpdateData(dashboardData.Data.DailyMission); + await TodayPerformancePieChartRef.UpdateData(dashboardData.Data.TodayPerformance); + await WeekyPerformancePieChartRef.UpdateData(dashboardData.Data.ThisWeekPerformance); + await MissionsPerformanceBarChartRef.UpdateData(dashboardData.Data.TotalMissionPerformance); + await TaktTimeLineChartRef.UpdateData(dashboardData.Data.TaktTimeMissions); + } + else Snackbar.Add($"{(string.IsNullOrEmpty(dashboardData.Message) ? "Có lỗi xảy ra khi lấy dữ liệu dashboard" : dashboardData.Message)}"); + StateHasChanged(); + } + + private async Task DashboardHub_DashboardDataUpdated(DashboardDto data) + { + timeUpdate = data.TimeUpdate; + DailyDataRef.UpdateData(data.DailyMission); + await TodayPerformancePieChartRef.UpdateData(data.TodayPerformance); + await WeekyPerformancePieChartRef.UpdateData(data.ThisWeekPerformance); + await MissionsPerformanceBarChartRef.UpdateData(data.TotalMissionPerformance); + await TaktTimeLineChartRef.UpdateData(data.TaktTimeMissions); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Pages/Dashboard.razor.css b/RobotNet.WebApp/Pages/Dashboard.razor.css new file mode 100644 index 0000000..8e65a8c --- /dev/null +++ b/RobotNet.WebApp/Pages/Dashboard.razor.css @@ -0,0 +1,115 @@ +.dashboard-title { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: 20px; + height: 54px; + background: linear-gradient(to right, rgb(5, 39, 103) 0%, #3a0647 70%); + border-radius: 15px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + padding: 0 1.5rem; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + .dashboard-title:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); + } + + .dashboard-title::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: linear-gradient(to right, rgb(5, 39, 103) 0%, #3a0647 70%); + opacity: 0; + transition: opacity 0.3s ease; + } + + .dashboard-title span { + font-family: var(--dashboard-text-font-family); + color: var(--dashboard-text-white-color); + font-size: 40px; + align-items: center; + display: flex; + margin-left: 10px; + } + + .dashboard-title .button-group { + display: flex; + flex-direction: row; + margin-right: 10px; + align-items: center; + } + + +.dashboard-content { + display: flex; + width: 100%; + flex-grow: 1; + flex-direction: column; + position: relative; +} + + .dashboard-content .daily-data { + display: flex; + width: 100%; + height: 18%; + margin-bottom: 20px; + } + + .dashboard-content .charts { + display: flex; + width: 100%; + flex-grow: 1; + height: 82%; + flex-direction: row; + position: relative; + margin-right: 20px; + } + + .dashboard-content .charts .performance-chart { + display: flex; + width: 36.9%; + height: 100%; + flex-direction: column; + margin-right: 20px; + } + + .dashboard-content .charts .performance-chart .daily-performance-chart { + width: 100%; + height: 48.5%; + margin-bottom: 20px; + } + + .dashboard-content .charts .performance-chart .weeky-performance-chart { + width: 100%; + height: 48.5%; + margin-bottom: 5px; + } + + + .dashboard-content .charts .weely-overall-chart { + display: flex; + flex-grow: 1; + width: 62%; + height: 100%; + flex-direction: column; + } + + .dashboard-content .charts .weely-overall-chart .mission-bar-chart { + width: 100%; + height: 48.5%; + margin-bottom: 20px; + } + + .dashboard-content .charts .weely-overall-chart .takt-time-chart { + width: 100%; + height: 48.5%; + margin-bottom: 5px; + } \ No newline at end of file diff --git a/RobotNet.WebApp/Pages/Logs.razor b/RobotNet.WebApp/Pages/Logs.razor new file mode 100644 index 0000000..676e57c --- /dev/null +++ b/RobotNet.WebApp/Pages/Logs.razor @@ -0,0 +1,192 @@ +@page "/logs" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares.Models + +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory +@inject IConfiguration Configuration +@inject ISnackbar Snackbar + +Logs + +
+
+ + +
+ + @foreach (var proccess in LogsProccess) + { + @proccess.Name + } + + + + + + + + + +
+
+
+ + + +
+
+ @if (ShowRawLog) + { +
+
Normal
+
+ + @foreach (var log in ShowLogs) + { + @log
+ } +
+ } + else + { + @if (SearchLogs.Count < ShowLogs.Count) + { +
+
Raw log
+
+ } + + @foreach (var log in SearchLogs) + { +
+ + @log.Time @log.Level + + @log.Message + @if (log.HasException) + { +
+
+                                    @log.Exception
+                                                                    
+ } +
+ } + } +
+
+
+
+ + +@code { + private DateTime DateLog = DateTime.Today; + private bool IsLoading; + private readonly List ShowLogs = new(); + private readonly List SearchLogs = new(); + private ElementReference LogContainerRef { get; set; } + private bool ShowRawLog { get; set; } + private string? FilterLog { get; set; } + + private List LogsProccess = []; + private LogConfiguration LogProccessSelected = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + LogsProccess = Configuration.GetSection("Logs").Get>() ?? []; + LogProccessSelected = LogsProccess.FirstOrDefault() ?? new LogConfiguration(); + await LoadLogs(); + } + + private async Task LoadLogs() + { + try + { + IsLoading = true; + ShowLogs.Clear(); + StateHasChanged(); + + using var Http = new HttpClient(); + var logs = await Http.GetFromJsonAsync>($"{LogProccessSelected.Url}?date={DateLog}"); + ShowLogs.AddRange(logs ?? []); + + IsLoading = false; + StateHasChanged(); + + await ReloadLogs(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task ReloadLogs() + { + IsLoading = true; + SearchLogs.Clear(); + StateHasChanged(); + + foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000)) + { + try + { + var log = System.Text.Json.JsonSerializer.Deserialize(line); + if (log is not null) SearchLogs.Add(log); + } + catch (System.Text.Json.JsonException) + { + continue; + } + } + + IsLoading = false; + StateHasChanged(); + await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef); + } + + private async Task OnSearch(string text) + { + FilterLog = text; + await ReloadLogs(); + } + + private async Task OnDateChanged(DateTime? date) + { + if (date is not null && date.HasValue) + { + DateLog = date.Value; + await LoadLogs(); + } + } + + public class LogConfiguration + { + public string Name { get; set; } = ""; + public string Url { get; set; } = ""; + } + + private async Task ExportLogs() + { + try + { + using var Http = new HttpClient(); + var fileContent = await Http.GetFromJsonAsync>($"{LogProccessSelected.Url}?date={DateLog}"); + var formattedContent = string.Join("\n", fileContent ?? []); + var fileName = $"{LogProccessSelected.Name}_{DateLog.ToShortDateString()}.txt"; + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, formattedContent, "text/plain"); + } + catch (Exception ex) + { + Snackbar.Add($"Lỗi khi tải file: {ex.Message}", Severity.Warning); + } + } +} + diff --git a/RobotNet.WebApp/Pages/Logs.razor.css b/RobotNet.WebApp/Pages/Logs.razor.css new file mode 100644 index 0000000..2a3ae7a --- /dev/null +++ b/RobotNet.WebApp/Pages/Logs.razor.css @@ -0,0 +1,38 @@ +.log-container { + height: 100%; + width: 100%; + overflow-x: hidden; + overflow-y: auto; + position: absolute; + top: 0px; + left: 0px; + display: flex; + flex-direction: column; +} + +.log { + word-wrap: break-word; + line-height: 18px; + margin-bottom: 12px; +} + +.log-logger { + color: rgba(0, 0, 0, 0.3); + font-size: 12px; +} + +.log-level { + display: inline-block; + width: 46px; +} + +.log-head { + border-radius: 3px; + padding: 2px 5px; +} + +.log-exception { + line-height: 16px; + margin-left: 30px; + color: crimson; +} diff --git a/RobotNet.WebApp/Pages/Missions.razor b/RobotNet.WebApp/Pages/Missions.razor new file mode 100644 index 0000000..4ea2b6e --- /dev/null +++ b/RobotNet.WebApp/Pages/Missions.razor @@ -0,0 +1,303 @@ +@page "/missions" +@attribute [Authorize] + +@using RobotNet.Script.Shares +@using RobotNet.Shares +@using RobotNet.Clients +@using RobotNet.WebApp.Scripts.Models + +@inject IHttpClientFactory httpFactory +@inject IDialogService DialogService +@inject ISnackbar Snackbar + +Runner Missions + +
+ + +
+

Danh sách Missions

+ + + +
+
+ + + + + + + + + + + + STT + Tên + Id + Create at + Parameters + Status + Log + + + + @context.Index + @context.MissionName + @context.Id.ToString()[..13] + @context.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") + + @if (context.Parameters?.Any() == true) + { + + @ShortenLog(context.Parameters, 30) + + + } + else + { + {} + } + + + @if (context.Status == MissionStatus.Running) + { + @context.Status + } + else if (context.Status == MissionStatus.Completed) + { + @context.Status + } + else if (context.Status == MissionStatus.Error) + { + @context.Status + } + else + { + @context.Status + } + + + @if (!string.IsNullOrEmpty(context.Log)) + { + + @ShortenLog(context.Log, 30) + + + } + + + @if (context.Status == MissionStatus.Running) + { + + } + else if (context.Status == MissionStatus.Paused) + { + + } + + @if (context.Status == MissionStatus.Running || context.Status == MissionStatus.Paused) + { + + } + + + + No matching records found + + + Loading... + + + + +
+
+ + + + Mission Log + +
@_selectedLog
+
+
+ + Close + +
+ + + + Parameters of mission "@_missionName" + + + + + + + + + + @foreach (var kv in _selectedParameters) + { + + + + + } + +
KeyValue
@kv.Key@kv.Value
+
+
+ + Close + +
+ +@code { + private string TableHeight = "0px"; + private ElementReference divRef; + private ElementReference toolbarRef; + private MudTable table = default!; + private List runnerMissions = []; + private string searchString = ""; + + private string _missionName = ""; + private bool _parametersDialogOpen = false; + private IDictionary _selectedParameters = new Dictionary(); + + private bool _logDialogOpen = false; + private string _selectedLog = string.Empty; + + private HttpClient http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + var rect = await divRef.MudGetBoundingClientRectAsync(); + var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync(); + TableHeight = $"{rect.Height - 70 - Math.Max(toolbarRect.Height, 64)}px"; + StateHasChanged(); + + await table.ReloadServerData(); + } + } + + private async Task> ServerReload(TableState state, CancellationToken token) + { + if (http == null) + { + http = httpFactory.CreateClient("ScriptManagerAPI"); + } + + var url = $"api/ScriptMissions/Runner?txtSearch={Uri.EscapeDataString(searchString)}&page={state.Page + 1}&size={state.PageSize}"; + var response = await http.GetFromJsonAsync>(url) ?? new(); + + return new TableData() + { + TotalItems = response.Total, + Items = [.. response.Items.Select((item, index) => new InstanceMissionModel(index + 1 + state.Page * state.PageSize, item))] + }; + } + + private void ShowParametersDialog(InstanceMissionModel mission) + { + try + { + _selectedParameters = System.Text.Json.JsonSerializer.Deserialize>(mission.Parameters) + ?? throw new Exception($"Can not convert parameters data from \"{mission.Parameters}\""); + _missionName = mission.MissionName; + _parametersDialogOpen = true; + StateHasChanged(); + } + catch (Exception ex) + { + Snackbar.Add($"Failed to parse parameters: {ex.Message}", Severity.Error); + } + } + + private string ShortenLog(string? log, int maxLength) + { + if (string.IsNullOrEmpty(log)) return string.Empty; + return log.Length > maxLength ? log.Substring(0, maxLength) + "..." : log; + } + + private void ShowLogDialog(string log) + { + _selectedLog = log; + _logDialogOpen = true; + StateHasChanged(); + } + + private async Task OnSearch(string text) + { + searchString = text; + await table.ReloadServerData(); + } + + private async Task CancelMission(InstanceMissionModel model) + { + var response = await http.DeleteFromJsonAsync($"api/ScriptMissions/Runner/{model.Id}"); + if (response == null) + { + Snackbar.Add("Failed to cancel mission: server response error", Severity.Error); + } + else if (response.IsSuccess) + { + Snackbar.Add("Mission canceled successfully", Severity.Success); + await table.ReloadServerData(); + } + else + { + Snackbar.Add($"Failed to cancel mission: {response.Message}", Severity.Error); + } + } + + private async Task PauseMission(InstanceMissionModel model) + { + var response = await http.PutFromJsonAsync($"api/ScriptMissions/Runner/{model.Id}/pause", new object()); + if (response == null) + { + Snackbar.Add("Failed to pause mission: server response error", Severity.Error); + } + else if (response.IsSuccess) + { + Snackbar.Add("Mission paused successfully", Severity.Success); + await table.ReloadServerData(); + } + else + { + Snackbar.Add($"Failed to pause mission: {response.Message}", Severity.Error); + } + } + + private async Task ResumeMission(InstanceMissionModel model) + { + var response = await http.PutFromJsonAsync($"api/ScriptMissions/Runner/{model.Id}/resume", new object()); + if (response == null) + { + Snackbar.Add("Failed to resume mission: server response error", Severity.Error); + } + else if (response.IsSuccess) + { + Snackbar.Add("Mission resumed successfully", Severity.Success); + await table.ReloadServerData(); + } + else + { + Snackbar.Add($"Failed to resume mission: {response.Message}", Severity.Error); + } + } + + private async Task ReloadTable() + { + await table.ReloadServerData(); + } +} diff --git a/RobotNet.WebApp/Pages/NavigationMapEditor.razor b/RobotNet.WebApp/Pages/NavigationMapEditor.razor new file mode 100644 index 0000000..b529af6 --- /dev/null +++ b/RobotNet.WebApp/Pages/NavigationMapEditor.razor @@ -0,0 +1,196 @@ +@page "/navigation-maps/editor/{Id:guid}" +@attribute [Authorize] + +@using RobotNet.MapShares.Dtos +@using RobotNet.MapShares.Enums +@using RobotNet.Shares +@using RobotNet.WebApp.Maps.Components.Editor +@using RobotNet.WebApp.Maps.Components.Toolbar + +@inject IHttpClientFactory HttpClientFactory +@inject IJSRuntime JSRuntime + +Map Designer + +
+ + +
+ +
+ +
+
+
+
+ @OverlayIsStr +
+
+
+
+ +@code { + [Parameter, EditorRequired] + public Guid Id { get; set; } + + private EditorToolbar EditorToolbarRef = null!; + private MapContainer MapContainerRef = null!; + + private bool ShowGrid = true; + private bool ShowName = true; + private bool ShowMapSlam = true; + private bool MapIsActive; + private bool MultiselectedEdge = false; + private bool MultiselectedNode = false; + private bool ZoneSelected = false; + private bool NodesUndoable = false; + private EditorState EditorState = EditorState.View; + private ZoneType ZoneType = ZoneType.Operating; + + private bool OverlayIsShow = false; + private string OverlayIsStr = "Loading..."; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + await LoadMap(); + } + + private async Task LoadMap() + { + try + { + OverlayIsShow = true; + StateHasChanged(); + + var mapDataResult = await Http.GetFromJsonAsync>($"api/MapsData/{Id}"); + if (mapDataResult is not null && mapDataResult.Data is not null) + { + MapIsActive = mapDataResult.Data.Active; + await MapContainerRef.LoadMap(mapDataResult.Data); + OverlayIsShow = false; + } + else OverlayIsStr = "Map Not Existed"; + StateHasChanged(); + } + catch + { + OverlayIsStr = "Map Not Existed"; + StateHasChanged(); + return; + } + } + + private void EditorExtensionChanged((ControlState state, bool value) extension) + { + switch (extension.state) + { + case ControlState.ShowName: + ShowName = extension.value; + break; + case ControlState.ShowGrid: + ShowGrid = extension.value; + break; + case ControlState.ShowMapSlam: + ShowMapSlam = extension.value; + break; + } + StateHasChanged(); + } + + private async Task ControlStateClick(ControlState state) + { + switch (state) + { + case ControlState.Undo: + MapContainerRef.UndoEditorBackup(); + break; + case ControlState.FitScreen: + await MapContainerRef.ScaleFitContentAsync(); + break; + case ControlState.Save: + await MapContainerRef.SaveChanged(); + break; + case ControlState.ZoomIn: + await MapContainerRef.ScaleZoom(0.5); + break; + case ControlState.ZoomOut: + await MapContainerRef.ScaleZoom(-0.5); + break; + case ControlState.Delete: + if (EditorState == EditorState.NavigationEdit || EditorState == EditorState.Scaner) await MapContainerRef.DeleteEdge(); + else if (EditorState == EditorState.SettingZone) await MapContainerRef.DeleteZone(); + break; + case ControlState.CheckMap: + await MapContainerRef.CheckMap(); + break; + } + } + + private async Task EditorStateChanged(EditorState state) + { + if (EditorState != state) + { + EditorState = state; + await MapContainerRef.EditStateChange(); + EditorToolbarRef.SetEditorState(state); + StateHasChanged(); + } + } + + private async Task AlignStateClick(AlignState state) + { + switch (state) + { + case AlignState.HorizontalLeft: + MapContainerRef.HorizontalLeft(); + break; + case AlignState.HorizontalRight: + MapContainerRef.HorizontalRight(); + break; + case AlignState.VerticalTop: + MapContainerRef.VerticalTop(); + break; + case AlignState.VerticalBottom: + MapContainerRef.VerticalBottom(); + break; + case AlignState.HorizontalCenter: + MapContainerRef.HorizontalCenter(); + break; + case AlignState.VerticalCenter: + MapContainerRef.VerticalCenter(); + break; + case AlignState.MergeNode: + await MapContainerRef.MergeNode(); + break; + case AlignState.SplitNode: + await MapContainerRef.SplitNode(); + break; + } + } + + private void MapIsChecking(bool state) + { + if (state) + { + OverlayIsShow = true; + OverlayIsStr = "Map Checking ..."; + } + else OverlayIsShow = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Pages/NavigationMapElement.razor b/RobotNet.WebApp/Pages/NavigationMapElement.razor new file mode 100644 index 0000000..46842d6 --- /dev/null +++ b/RobotNet.WebApp/Pages/NavigationMapElement.razor @@ -0,0 +1,103 @@ +@page "/navigation-maps/elements/{Id:guid}" +@attribute [Authorize] + +@using RobotNet.MapShares.Dtos +@using RobotNet.Shares +@using RobotNet.WebApp.Maps.Components.Element + +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + +Map Element + +
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ + + Is Loadding ... + +
+ +@code { + [Parameter, EditorRequired] + public Guid Id { get; set; } + + public List ElementModels { get; set; } = []; + public List Elements { get; set; } = []; + + private bool IsLoading { get; set; } + private MapInfoDto MapInfo = new(); + + private ElementDefaultProperty ElementDefaultPropertyRef = default!; + private ElementImage ElementImageRef = default!; + private MapElement MapElementRef = default!; + private ElementPropertyTable ElementPropertyRef = default!; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + var reponse = await Http.GetFromJsonAsync>($"api/MapsManager/{Id}"); + if (reponse is not null && reponse.Data is not null) MapInfo = reponse.Data; + await LoadElementModels(); + StateHasChanged(); + } + + private async Task LoadElementModels() + { + IsLoading = true; + ElementModels.Clear(); + StateHasChanged(); + + var elModels = await Http.GetFromJsonAsync>>($"api/ElementModels/map/{Id}"); + if (elModels is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!elModels.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {elModels.Message}", Severity.Error); + else if (elModels.Data is null || !elModels.Data.Any()) + { + Snackbar.Add("Không có mô hình phần tử nào được tìm thấy", Severity.Warning); + } + else + { + ElementModels.AddRange(elModels.Data.OrderBy(el => el.Name)); + } + IsLoading = false; + StateHasChanged(); + } + + private async Task ElementModelSelectChanged(ElementModelDto? model) + { + ElementDefaultPropertyRef.SetElementModel(model); + ElementImageRef.SetElementModel(model); + await MapElementRef.SetData(model); + } + + private void ElementSelectChanged(ElementDto? model) => ElementPropertyRef.SetProperties(model); +} diff --git a/RobotNet.WebApp/Pages/NavigationMapSetting.razor b/RobotNet.WebApp/Pages/NavigationMapSetting.razor new file mode 100644 index 0000000..01cb208 --- /dev/null +++ b/RobotNet.WebApp/Pages/NavigationMapSetting.razor @@ -0,0 +1,116 @@ +@page "/navigation-maps/settings/{Id:guid}" +@attribute [Authorize] + +@using RobotNet.MapShares.Dtos +@using RobotNet.Shares +@using RobotNet.WebApp.Maps.Components.Setting + +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory +@inject NavigationManager Nav + +Map Setting + +
+
+ + + +
+ +
+
+ + + +
+
+ @MapName + + @if (PanelSelected == 1) + { + + + + + + + } + + + + + + + + + +
+
+
+
+ + +@code { + [Parameter, EditorRequired] + public Guid Id { get; set; } + + private MapSettingDefault MapConfigDefaultRef = null!; + private MapSettingAction MapConfigActionRef = null!; + private int PanelSelected = 0; + + private string MapName = ""; + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + var reponse = await Http.GetFromJsonAsync>($"api/MapsManager/{Id}"); + if (reponse is not null && reponse.Data is not null) MapName = reponse.Data.Name; + StateHasChanged(); + } + + private async Task OnSave() + { + switch (PanelSelected) + { + case 0: + await MapConfigDefaultRef.Save(); + break; + case 1: + await MapConfigActionRef.Save(); + break; + } + } + + private void OnReset() + { + switch (PanelSelected) + { + case 0: + MapConfigDefaultRef.Reset(); + break; + case 1: + MapConfigActionRef.Reset(); + break; + } + } + + private void ActivePanelChanged(int value) + { + PanelSelected = value; + } + + private void CreateAction() + { + MapConfigActionRef.ShowCreateAction(); + } + + private async Task DeleteAction() + { + await MapConfigActionRef.DeleteAction(); + } +} diff --git a/RobotNet.WebApp/Pages/NavigationMapsManager.razor b/RobotNet.WebApp/Pages/NavigationMapsManager.razor new file mode 100644 index 0000000..a3c944a --- /dev/null +++ b/RobotNet.WebApp/Pages/NavigationMapsManager.razor @@ -0,0 +1,585 @@ +@page "/navigation-maps" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares +@using RobotNet.MapShares.Dtos +@using System.Net.Http.Headers +@using RobotNet.Shares +@using RobotNet.WebApp.Maps.Components + +@inject NavigationManager NavManager +@inject IHttpClientFactory HttpClientFactory +@inject IJSRuntime JSRuntime +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +Map Manager + + + +
+
+
+ + + IMPORT + + + NEW + +
+
+ + + Nr + Name + Width (m) + Height (m) + Resolution (m/px) + OriginX + OriginY + + + + + @(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1) + + + @context.Name + + + @context.Width + + + @context.Height + + + @context.Resolution + + + @context.OriginX + + + @context.OriginY + + +
+ + Edit + Delete + + Active +
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+ + + + + Create New Map + + + + + +
+ + +
+ +
+
+
+
+
+ Map Image +
+
+
+ +
+
+ + Cancel + Create + +
+ + + + + Update Map + + + + + +
+ + +
+ +
+
+ + Cancel + Update + +
+ + + + + Import New Map + + + + + +
+ + +
+ +
+
+ Map Image +
+
+
+
+ + Cancel + Create + +
+ +@code { + private string txtSearch = ""; + private bool IsLoading = false; + + private List Maps = []; + private List MapsShow = []; + private MudTable? Table; + + private int selectedRowNumber = -1; + private MapInfoDto MapSelected = new(); + + private bool IsCreateMapVisible { get; set; } + private readonly MapCreateModel MapCreateModel = new(); + private IBrowserFile? MapCreateImage { get; set; } + private string? ProjectionImageFilePreview { get; set; } + + private bool IsUpdateMapVisible { get; set; } + private readonly MapUpdateModel MapUpdateModel = new(); + + private NavigationMapPreview NavigationMapPreviewRef = default!; + + private MapExportDto MapImport = default!; + private bool IsImportMapVisible { get; set; } + private string? ProjectionImportImageFilePreview { get; set; } + private HttpClient Http = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + Http = HttpClientFactory.CreateClient("MapManagerAPI"); + await LoadMaps(); + } + + private async Task LoadMaps() + { + try + { + IsLoading = true; + Maps.Clear(); + StateHasChanged(); + + var maps = await Http.GetFromJsonAsync>($"api/MapsManager?txtSearch={txtSearch}"); + Maps.AddRange(maps ?? []); + + Table?.ReloadServerData(); + IsLoading = false; + StateHasChanged(); + } + catch + { + return; + } + } + + private void TextSearchChanged(string text) + { + txtSearch = text; + Table?.ReloadServerData(); + } + + private bool FilterFunc(MapInfoDto map) + { + if (string.IsNullOrWhiteSpace(txtSearch)) + return true; + if (map.Name is not null && map.Name.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if ($"{map.Name}".Contains(txtSearch)) + return true; + return false; + } + + private Task> ReloadData(TableState state, CancellationToken _) + { + MapsShow.Clear(); + var tasks = new List(); + Maps.ForEach(task => + { + if (FilterFunc(task)) tasks.Add(task); + }); + MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); + return Task.FromResult(new TableData() { TotalItems = tasks.Count, Items = MapsShow }); + } + + private void RowClickEvent(TableRowClickEventArgs tableRowClickEventArgs) { } + + private string SelectedRowClassFunc(MapInfoDto element, int rowNumber) + { + if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && !Table.SelectedItem.Equals(element)) + { + return string.Empty; + } + else if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + return "selected"; + } + else if (Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + selectedRowNumber = rowNumber; + MapSelected = element; + NavigationMapPreviewRef.SetMapPreview(MapSelected); + return "selected"; + } + else + { + return string.Empty; + } + } + + private void OpenCreateMap() + { + MapCreateModel.Name = string.Empty; + MapCreateModel.OriginX = 0; + MapCreateModel.OriginY = 0; + MapCreateModel.Resolution = 0.1; + ProjectionImageFilePreview = "/images/Image-not-found.png"; + MapCreateImage = null; + IsCreateMapVisible = true; + StateHasChanged(); + } + + private async Task MapCreateImageChanged(InputFileChangeEventArgs e) + { + MapCreateImage = e.File; + if (MapCreateImage is not null) + { + var buffers = new byte[MapCreateImage.Size]; + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + await using var fs = new FileStream(path, FileMode.Create); + await MapCreateImage.OpenReadStream(MapCreateImage.Size).CopyToAsync(fs); + fs.Position = 0; + await fs.ReadAsync(buffers); + fs.Close(); + File.Delete(path); + + ProjectionImageFilePreview = $"data:{MapCreateImage.ContentType};base64,{Convert.ToBase64String(buffers)}"; + StateHasChanged(); + } + } + + private async Task CreateNewMap() + { + try + { + if (MapCreateImage is null || string.IsNullOrEmpty(ProjectionImageFilePreview)) + { + Snackbar.Add("Ảnh map chưa được chọn.", Severity.Warning); + return; + } + + if (string.IsNullOrEmpty(MapCreateModel.Name)) + { + Snackbar.Add("Map's name không được để trống.", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(MapCreateModel.Name); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + using var content = new MultipartFormDataContent + { + { new StringContent(MapCreateModel.Name ?? String.Empty), nameof(MapCreateModel.Name) }, + { new StringContent(MapCreateModel.OriginX.ToString()), nameof(MapCreateModel.OriginX) }, + { new StringContent(MapCreateModel.OriginY.ToString()), nameof(MapCreateModel.OriginY) }, + { new StringContent(MapCreateModel.Resolution.ToString()), nameof(MapCreateModel.Resolution) }, + }; + + var fileContent = new StreamContent(MapCreateImage.OpenReadStream(maxAllowedSize: 20480000)); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(MapCreateImage.ContentType); + content.Add(fileContent, "Image", MapCreateImage.Name); + + var response = await (await Http.PostAsync("api/MapsManager", content)).Content.ReadFromJsonAsync>(); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else if (response.Data is null) Snackbar.Add("Hệ thống không thể tạo bản đồ này", Severity.Error); + else + { + Maps.Add(response.Data); + Table?.ReloadServerData(); + Snackbar.Add($"Tạo map {MapCreateModel.Name} thành công.", Severity.Success); + } + + IsCreateMapVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private void OpenUpdateMap(MapInfoDto map) + { + MapUpdateModel.Id = map.Id; + MapUpdateModel.Name = map.Name; + MapUpdateModel.OriginX = map.OriginX; + MapUpdateModel.OriginY = map.OriginY; + MapUpdateModel.Resolution = map.Resolution; + IsUpdateMapVisible = true; + StateHasChanged(); + } + + private async Task UpdateMap() + { + var nameErrors = MapEditorHelper.NameChecking(MapUpdateModel.Name); + if (!nameErrors.IsSuccess) + { + Snackbar.Add(nameErrors.returnStr, Severity.Warning); + return; + } + + var result = await (await Http.PutAsJsonAsync($"api/MapsManager/{MapUpdateModel.Id}", MapUpdateModel)).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) + { + Snackbar.Add("Dữ liệu không hợp lệ", Severity.Error); + } + else + { + var map = Maps.FirstOrDefault(map => map.Id == MapUpdateModel.Id); + if (map is not null) + { + map.Name = result.Data.Name; + map.OriginX = result.Data.OriginX; + map.OriginY = result.Data.OriginY; + map.Resolution = result.Data.Resolution; + map.Width = result.Data.Width; + map.Height = result.Data.Height; + map.Active = result.Data.Active; + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + else Snackbar.Add("Map không tồn tại", Severity.Error); + } + + IsUpdateMapVisible = false; + StateHasChanged(); + } + + private async Task DeleteMap(Guid mapId) + { + var parameters = new DialogParameters + { + { x => x.Content, "Bạn có chắc chắn muốn xóa map đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Map", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + var response = await Http.DeleteFromJsonAsync($"api/MapsManager/{mapId}"); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var map = Maps.FirstOrDefault(m => m.Id == mapId); + if (map is not null) + { + if (map.Id == MapSelected.Id) + { + NavigationMapPreviewRef.SetMapPreview(null); + Table?.SetSelectedItem(null); + selectedRowNumber = -1; + } + Maps.Remove(map); + Table?.ReloadServerData(); + Snackbar.Add($"Xóa map thành công.", Severity.Success); + } + else Snackbar.Add($"Map không tồn tại.", Severity.Warning); + } + StateHasChanged(); + } + } + + private async Task MapActiveToggle(Guid mapId, bool value) + { + var result = await (await Http.PutAsJsonAsync($"api/MapsManager/active", new MapActiveModel() { Id = mapId, Active = value })).Content.ReadFromJsonAsync(); + if (result is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var map = Maps.FirstOrDefault(map => map.Id == mapId); + if (map is not null) + { + map.Active = value; + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + else Snackbar.Add($"Map không tồn tại.", Severity.Warning); + } + StateHasChanged(); + } + + private async Task MapImageChangedCallBack(Guid mapId) + { + try + { + var mapupdate = await Http.GetFromJsonAsync>($"api/MapsManager/{mapId}"); + if (mapupdate is not null && mapupdate.Data is not null) + { + var map = Maps.FirstOrDefault(m => m.Id == mapupdate.Data.Id); + if (map is not null) + { + map.Name = mapupdate.Data.Name; + map.Width = mapupdate.Data.Width; + map.Height = mapupdate.Data.Height; + } + } + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task OnMapImportChanged(InputFileChangeEventArgs e) + { + if (e.File is not null) + { + using var content = new MultipartFormDataContent(); + var fileContent = new StreamContent(e.File.OpenReadStream(maxAllowedSize: 10240000)); + if (!string.IsNullOrEmpty(e.File.ContentType)) fileContent.Headers.ContentType = new MediaTypeHeaderValue(e.File.ContentType); + content.Add(fileContent, "importmap", e.File.Name); + var response = await (await Http.PostAsync("api/MapExport/decrypt", content)).Content.ReadFromJsonAsync>(); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {response.Message}", Severity.Error); + else + { + if (response.Data is not null) + { + MapImport = response.Data; + ProjectionImportImageFilePreview = $"data:image/png;base64,{Convert.ToBase64String(response.Data.Data.ImageData)}"; + IsImportMapVisible = true; + } + else Snackbar.Add("Dữ liệu không hợp lệ", Severity.Warning); + } + StateHasChanged(); + } + } + + private async Task ImportNewMap() + { + if (string.IsNullOrEmpty(MapImport.Name)) + { + Snackbar.Add("Map's name không được để trống.", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(MapImport.Name); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + var response = await (await Http.PostAsJsonAsync("api/MapsManager/import", MapImport)).Content.ReadFromJsonAsync>(); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); + else if (!response.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {response.Message}", Severity.Warning); + else + { + if (response.Data is not null) + { + Maps.Add(response.Data); + Table?.ReloadServerData(); + Snackbar.Add($"Import bản đồ thành công!", Severity.Success); + } + else Snackbar.Add("Dữ liệu không hợp lệ", Severity.Warning); + } + MapImport = default!; + IsImportMapVisible = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Pages/NavigationMapsManager.razor.css b/RobotNet.WebApp/Pages/NavigationMapsManager.razor.css new file mode 100644 index 0000000..adbe949 --- /dev/null +++ b/RobotNet.WebApp/Pages/NavigationMapsManager.razor.css @@ -0,0 +1,21 @@ +.map-preview{ + width: 20%; + height: 100%; + border-left: 1px solid silver; +} + +.map-create-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .map-create-item img { + justify-content: center; + width: 400px; + height: 300px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } diff --git a/RobotNet.WebApp/Pages/OpenACSSettings.razor b/RobotNet.WebApp/Pages/OpenACSSettings.razor new file mode 100644 index 0000000..37418f3 --- /dev/null +++ b/RobotNet.WebApp/Pages/OpenACSSettings.razor @@ -0,0 +1,71 @@ +@page "/open-acs" +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.RobotShares.OpenACS +@using RobotNet.Shares +@using RobotNet.WebApp.Robots.Components.OpenACS +@attribute [Authorize] + +@inject IHttpClientFactory HttpClientFactory + +Open ACS + +
+
+ + + + +
+
+
+ +
+
+ +
+
+ + + Loadding... + +
+ +@code { + private OpenACSSettingsDto PublishSettingDto = new(); + private bool OverlayIsVisible = false; + + private PublishSetting PublishSettingRef = default!; + private TrafficSetting TrafficSettingRef = default!; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await LoadSettings(); + } + + private async Task LoadSettings() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + using var client = HttpClientFactory.CreateClient("RobotManagerAPI"); + var settings = await client.GetFromJsonAsync>("api/OpenACSSettings"); + if (settings is null || settings.Data is null || !settings.IsSuccess) PublishSettingDto = new(); + else PublishSettingDto = settings.Data; + + PublishSettingRef.UpdateSettings(PublishSettingDto.PublishSetting); + TrafficSettingRef.UpdateSettings(PublishSettingDto.TrafficSetting); + + OverlayIsVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } +} diff --git a/RobotNet.WebApp/Pages/RobotDetail.razor b/RobotNet.WebApp/Pages/RobotDetail.razor new file mode 100644 index 0000000..752b90d --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotDetail.razor @@ -0,0 +1,108 @@ +@page "/robots/detail/{RobotId}" +@attribute [Authorize] + +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.RobotShares.Dtos +@using RobotNet.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Robots.Components.Robot + +@inject RobotHubClient RobotHub +@inject IHttpClientFactory HttpFactory + +Robot Detail + +
+
+ + +
+
+ +
+ + + @DeactiveStr + +
+ +@code { + [Parameter] + public string RobotId { get; set; } = ""; + + private RobotDto Robot = new(); + private RobotInfomation RobotInfomationRef = default!; + private RobotAction RobotActionRef = default!; + private RobotTestRandom RobotTestRandomRef = default!; + private bool IsDeactive; + private string DeactiveStr = "Robot Empty"; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + RobotHub.VDA5050InfoChanged += VDA5050InfoChanged; + RobotHub.RobotDetailDeactive += RobotDetailDeactive; + await RobotHub.StartAsync(); + + await LoadRobot(); + } + + private async Task LoadRobot() + { + try + { + IsDeactive = true; + StateHasChanged(); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var result = await Http.GetFromJsonAsync>($"api/Robots/{RobotId}"); + if (result is null || result.Data is null || !result.IsSuccess) return; + Robot = result.Data; + + await RobotHub.RobotDetailActive(RobotId); + + IsDeactive = false; + StateHasChanged(); + await RobotActionRef.LoadNodes(); + await RobotActionRef.LoadActionAsync(); + await RobotTestRandomRef.LoadNodes(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private void VDA5050InfoChanged(RobotVDA5050StateDto state) + { + if (state.RobotId == RobotId) + { + Robot.Online = state.Online; + RobotInfomationRef.UpdateState(state); + RobotActionRef.UpdateState(state.Online, state.IsWorking); + RobotTestRandomRef.UpdateState(state.Online, state.IsWorking); + } + StateHasChanged(); + } + + private void RobotDetailDeactive() + { + DeactiveStr = "Deactive"; + IsDeactive = true; + StateHasChanged(); + RobotHub.VDA5050InfoChanged -= VDA5050InfoChanged; + RobotHub.RobotDetailDeactive -= RobotDetailDeactive; + _ = RobotHub.StopAsync(); + } + + public async ValueTask DisposeAsync() + { + RobotHub.VDA5050InfoChanged -= VDA5050InfoChanged; + RobotHub.RobotDetailDeactive -= RobotDetailDeactive; + await RobotHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Pages/RobotManager.razor b/RobotNet.WebApp/Pages/RobotManager.razor new file mode 100644 index 0000000..11cea4b --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotManager.razor @@ -0,0 +1,315 @@ +@page "/robots" +@attribute [Authorize] +@implements IAsyncDisposable + +@using RobotNet.MapShares +@using RobotNet.RobotShares.Dtos +@using RobotNet.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Robots.Components + +@inject ISnackbar Snackbar +@inject IDialogService Dialog +@inject IHttpClientFactory HttpFactory +@inject IJSRuntime JSRuntime +@inject NavigationManager NavManager +@inject RobotHubClient RobotHub + +Robot Manager + +
+
+ + + Simulation + + NEW + +
+
+ + + Name + Id + Model + Map + NavigationType + Online + State + Pin + + + + @context.Name + @context.RobotId + @context.ModelName + @context.MapName + @context.NavigationType + + + + @context.State + @($"{context.Battery} %") + +
+ + +
+
+
+ + + +
+
+
+ + + + + Create New Robot + + + + + + + + + @foreach (var model in ListRobotModels) + { + @model.ModelName + } + + + + + + Cancel + Create + + + +@code { + private string? txtSearch { get; set; } + private bool IsLoading { get; set; } + + private MudTable? Table; + private readonly List Robots = new(); + private List RobotsShow = new(); + private HashSet RobotSelecteds = []; + + private bool IsCreateRobotVisible { get; set; } + private readonly RobotCreateModel RobotCreate = new(); + private readonly List ListRobotModels = new(); + private RobotModelDto? RobotModelSelected { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await LoadRobots(); + RobotHub.IsOnlineChanged += RobotIsOnlineChanged; + await RobotHub.StartAsync(); + } + + private void RobotIsOnlineChanged(IEnumerable robots) + { + foreach (var robot in robots) + { + var robotDto = Robots.FirstOrDefault(r => r.RobotId == robot.RobotId); + if (robotDto is null) continue; + robotDto.Online = robot.IsOnline; + robotDto.State = robot.State; + robotDto.Battery = robot.Battery; + } + StateHasChanged(); + } + + private async Task LoadRobots() + { + IsLoading = true; + Robots.Clear(); + StateHasChanged(); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var robots = await Http.GetFromJsonAsync>>("api/Robots"); + if (robots is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!robots.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {robots.Message}", Severity.Error); + else if (robots.Data is null || !robots.Data.Any()) + { + Snackbar.Add("Không có robot nào", Severity.Warning); + RobotsShow.Clear(); + Table?.ReloadServerData(); + } + else + { + Robots.AddRange(robots.Data.OrderByDescending(robot => robot.Online)); + Table?.ReloadServerData(); + } + IsLoading = false; + StateHasChanged(); + } + + private bool FilterFunc(RobotDto robot) + { + if (string.IsNullOrWhiteSpace(txtSearch)) + return true; + if (robot.Name.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if (robot.RobotId.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if (robot.ModelName.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if (robot.MapName.ToString().Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if (robot.State.ToString().Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if (robot.NavigationType.ToString().Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + private Task> ReloadData(TableState state, CancellationToken _) + { + var robots = new List(); + Robots.ForEach(robot => + { + if (FilterFunc(robot)) robots.Add(robot); + }); + RobotsShow = robots.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); + return Task.FromResult(new TableData() { TotalItems = robots.Count(), Items = RobotsShow }); + } + + private async Task DeleteRobot(RobotDto robot) + { + var parameters = new DialogParameters + { + { x => x.Content, "Bạn có chắc chắn muốn xóa robot đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Robot Model", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var delete = await Http.DeleteFromJsonAsync($"api/Robots/{robot.RobotId}"); + if (delete is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!delete.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {delete.Message}", Severity.Error); + else + { + Robots.Remove(robot); + Snackbar.Add("Xóa robot thành công!", Severity.Success); + Table?.ReloadServerData(); + } + StateHasChanged(); + } + } + + private void TextSearchChanged(string text) + { + txtSearch = text; + Table?.ReloadServerData(); + } + + private async Task OpenCreateRobot() + { + ListRobotModels.Clear(); + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var robotmodels = await Http.GetFromJsonAsync>($"api/RobotModels?txtSearch={txtSearch}"); + if (robotmodels is null || robotmodels.Count() == 0) + { + Snackbar.Add("Vui lòng tạo robot model", Severity.Error); + return; + } + else ListRobotModels.AddRange(robotmodels); + + RobotModelSelected = ListRobotModels.First(); + RobotCreate.Name = ""; + RobotCreate.RobotId = ""; + RobotCreate.ModelId = Guid.Empty; + IsCreateRobotVisible = true; + StateHasChanged(); + } + + private async Task CreateRobot() + { + if (RobotModelSelected is null) + { + Snackbar.Add("Hãy chọn robot model!", Severity.Warning); + return; + } + if (string.IsNullOrEmpty(RobotCreate.Name)) + { + Snackbar.Add("Trường tên không được để trống", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(RobotCreate.Name); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + if (string.IsNullOrEmpty(RobotCreate.RobotId)) + { + Snackbar.Add("Trường RobotId không được để trống", Severity.Warning); + return; + } + + RobotCreate.ModelId = RobotModelSelected.Id; + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var create = await (await Http.PostAsJsonAsync("api/Robots", RobotCreate)).Content.ReadFromJsonAsync>(); + if (create is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!create.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {create.Message}", Severity.Error); + else if (create.Data is null) Snackbar.Add("Tạo robot không thành công", Severity.Error); + else + { + Robots.Add(new() + { + Id = create.Data.Id, + RobotId = create.Data.RobotId, + Name = create.Data.Name, + ModelId = create.Data.ModelId, + ModelName = create.Data.ModelName, + NavigationType = create.Data.NavigationType, + }); + Table?.ReloadServerData(); + IsCreateRobotVisible = false; + Snackbar.Add("Tạo robot thành công!", Severity.Success); + } + StateHasChanged(); + } + + private async Task AddSimulation() + { + if (!RobotSelecteds.Any()) return; + var robotIds = RobotSelecteds.Select(r => r.RobotId).ToList(); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var onRobot = await (await Http.PostAsJsonAsync($"api/Robots/simulation", robotIds)).Content.ReadFromJsonAsync(); + if (onRobot is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!onRobot.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {onRobot.Message}", Severity.Error); + else Snackbar.Add("Khởi tạo thành công!", Severity.Success); + StateHasChanged(); + } + + public async ValueTask DisposeAsync() + { + RobotHub.IsOnlineChanged -= RobotIsOnlineChanged; + await RobotHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Pages/RobotModelManager.razor b/RobotNet.WebApp/Pages/RobotModelManager.razor new file mode 100644 index 0000000..dd5106b --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotModelManager.razor @@ -0,0 +1,464 @@ +@page "/robots/model" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares +@using RobotNet.RobotShares.Dtos +@using RobotNet.RobotShares.Enums +@using RobotNet.Shares +@using RobotNet.WebApp.Robots.Components +@using RobotNet.WebApp.Robots.Components.Robot +@using System.Net.Http.Headers + +@inject IHttpClientFactory HttpFactory +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +Robot Model + + + +
+
+
+ + + NEW + +
+
+ + + Nr + Name + Length (m) + Width (m) + NavigationPointX (m) + NavigationPointY (m) + NavigationType + + + + + @(Table?.CurrentPage * Table?.RowsPerPage + RobotModelsShow.IndexOf(context) + 1) + + + @context.ModelName + + + @context.Length + + + @context.Width + + + @context.OriginX + + + @context.OriginY + + + @context.NavigationType + + +
+ + Edit + Delete + +
+
+
+ +
+ +
+
+
+
+
+
+ +
+
+ + + + + Create New Robot Model + + + + + +
+ + +
+
+ + +
+ + @foreach (NavigationType type in Enum.GetValues(typeof(NavigationType))) + { + @type + } + +
+
+
+
+
+ Robot Model Image +
+
+
+ +
+
+ + Cancel + Create + +
+ + + + + Update Robot Model + + + + + +
+ + +
+
+ + +
+
+
+ + Cancel + Update + +
+ +@code { + private string txtSearch = ""; + private bool IsLoading = false; + + private RobotModelPreview RobotModelPreviewRef = default!; + + private List RobotModels = []; + private List RobotModelsShow = []; + private MudTable? Table; + + private int selectedRowNumber = -1; + private RobotModelDto RobotModelSelected = new(); + + private bool IsCreateRobotModelVisible { get; set; } + private readonly RobotModelCreateModel RobotModelCreateModel = new(); + private IBrowserFile? RobotModelCreateImage { get; set; } + private string? ProjectionImageFilePreview { get; set; } + + private bool IsUpdateRobotModelVisible { get; set; } + private readonly RobotModelUpdateModel RobotModelUpdateModel = new(); + + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await LoadRobotModels(); + } + + private async Task LoadRobotModels() + { + try + { + IsLoading = true; + RobotModels.Clear(); + StateHasChanged(); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var robotmodels = await Http.GetFromJsonAsync>($"api/RobotModels?txtSearch={txtSearch}"); + RobotModels.AddRange(robotmodels ?? []); + + Table?.ReloadServerData(); + IsLoading = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private void TextSearchChanged(string text) + { + txtSearch = text; + Table?.ReloadServerData(); + } + + private bool FilterFunc(RobotModelDto robotmodel) + { + if (string.IsNullOrWhiteSpace(txtSearch)) + return true; + if (robotmodel.ModelName is not null && robotmodel.ModelName.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + if ($"{robotmodel.ModelName}".Contains(txtSearch)) + return true; + return false; + } + + private Task> ReloadData(TableState state, CancellationToken _) + { + RobotModelsShow.Clear(); + var tasks = new List(); + RobotModels.ForEach(task => + { + if (FilterFunc(task)) tasks.Add(task); + }); + RobotModelsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); + return Task.FromResult(new TableData() { TotalItems = tasks.Count, Items = RobotModelsShow }); + } + + private void RowClickEvent(TableRowClickEventArgs tableRowClickEventArgs) { } + + private string SelectedRowClassFunc(RobotModelDto element, int rowNumber) + { + if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && !Table.SelectedItem.Equals(element)) + { + return string.Empty; + } + else if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + return "selected"; + } + else if (Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + selectedRowNumber = rowNumber; + RobotModelSelected = element; + RobotModelPreviewRef.SetRobotPreview(RobotModelSelected); + return "selected"; + } + else + { + return string.Empty; + } + } + + private void OpenCreateRobotModel() + { + RobotModelCreateModel.ModelName = string.Empty; + RobotModelCreateModel.OriginX = 0; + RobotModelCreateModel.OriginY = 0; + RobotModelCreateModel.Length = 0; + RobotModelCreateModel.Width = 0; + RobotModelCreateModel.NavigationType = NavigationType.Differential; + ProjectionImageFilePreview = "/images/Image-not-found.png"; + RobotModelCreateImage = null; + IsCreateRobotModelVisible = true; + StateHasChanged(); + } + + private async Task RobotModelCreateImageChanged(InputFileChangeEventArgs e) + { + RobotModelCreateImage = e.File; + if (RobotModelCreateImage is not null) + { + var buffers = new byte[RobotModelCreateImage.Size]; + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + await using var fs = new FileStream(path, FileMode.Create); + await RobotModelCreateImage.OpenReadStream(RobotModelCreateImage.Size).CopyToAsync(fs); + fs.Position = 0; + await fs.ReadAsync(buffers); + fs.Close(); + File.Delete(path); + + ProjectionImageFilePreview = $"data:{RobotModelCreateImage.ContentType};base64,{Convert.ToBase64String(buffers)}"; + StateHasChanged(); + } + } + + private async Task CreateNewRobotModel() + { + try + { + if (RobotModelCreateImage is null || string.IsNullOrEmpty(ProjectionImageFilePreview)) + { + Snackbar.Add("Ảnh robot chưa được chọn.", Severity.Warning); + return; + } + + if (string.IsNullOrEmpty(RobotModelCreateModel.ModelName)) + { + Snackbar.Add("Map's name không được để trống.", Severity.Warning); + return; + } + + var nameInvalid = MapEditorHelper.NameChecking(RobotModelCreateModel.ModelName); + if (!nameInvalid.IsSuccess) + { + Snackbar.Add(nameInvalid.returnStr, Severity.Warning); + return; + } + + using var content = new MultipartFormDataContent + { + { new StringContent(RobotModelCreateModel.ModelName ?? String.Empty), nameof(RobotModelCreateModel.ModelName) }, + { new StringContent(RobotModelCreateModel.OriginX.ToString()), nameof(RobotModelCreateModel.OriginX) }, + { new StringContent(RobotModelCreateModel.OriginY.ToString()), nameof(RobotModelCreateModel.OriginY) }, + { new StringContent(RobotModelCreateModel.Length.ToString()), nameof(RobotModelCreateModel.Length) }, + { new StringContent(RobotModelCreateModel.Width.ToString()), nameof(RobotModelCreateModel.Width) }, + { new StringContent(RobotModelCreateModel.NavigationType.ToString()), nameof(RobotModelCreateModel.NavigationType) } + }; + + var fileContent = new StreamContent(RobotModelCreateImage.OpenReadStream(maxAllowedSize: 2048000)); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(RobotModelCreateImage.ContentType); + content.Add(fileContent, "Image", RobotModelCreateImage.Name); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var response = await (await Http.PostAsync("api/RobotModels", content)).Content.ReadFromJsonAsync>(); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else if (response.Data is null) Snackbar.Add("Hệ thống không thể tạo robot model này", Severity.Error); + else + { + RobotModels.Add(response.Data); + Table?.ReloadServerData(); + Snackbar.Add($"Tạo robot model {RobotModelCreateModel.ModelName} thành công.", Severity.Success); + } + + IsCreateRobotModelVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private void OpenUpdateRobotModel(RobotModelDto robotmodel) + { + RobotModelUpdateModel.Id = robotmodel.Id; + RobotModelUpdateModel.ModelName = robotmodel.ModelName; + RobotModelUpdateModel.OriginX = robotmodel.OriginX; + RobotModelUpdateModel.OriginY = robotmodel.OriginY; + RobotModelUpdateModel.Length = robotmodel.Length; + RobotModelUpdateModel.Width = robotmodel.Width; + IsUpdateRobotModelVisible = true; + StateHasChanged(); + } + + private async Task UpdateRobotModel() + { + var nameErrors = MapEditorHelper.NameChecking(RobotModelUpdateModel.ModelName); + if (!nameErrors.IsSuccess) + { + Snackbar.Add(nameErrors.returnStr, Severity.Warning); + return; + } + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var result = await (await Http.PutAsJsonAsync($"api/RobotModels", RobotModelUpdateModel)).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) + { + Snackbar.Add("Hệ thống không thể cập nhật robot model này", Severity.Error); + } + else + { + var robotmodel = RobotModels.FirstOrDefault(rm => rm.Id == RobotModelUpdateModel.Id); + if (robotmodel is not null) + { + robotmodel.ModelName = result.Data.ModelName; + robotmodel.OriginX = result.Data.OriginX; + robotmodel.OriginY = result.Data.OriginY; + robotmodel.Width = result.Data.Width; + robotmodel.Length = result.Data.Length; + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + else Snackbar.Add("Robot Model không tồn tại", Severity.Error); + } + + IsUpdateRobotModelVisible = false; + StateHasChanged(); + } + + private async Task DeleteRobotModel(Guid robotId) + { + var parameters = new DialogParameters + { + { x => x.Content, "Bạn có chắc chắn muốn xóa robot model đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Robot Model", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var response = await Http.DeleteFromJsonAsync($"api/RobotModels/{robotId}"); + if (response is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!response.IsSuccess) Snackbar.Add(response.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + var robotmodel = RobotModels.FirstOrDefault(m => m.Id == robotId); + if (robotmodel is not null) + { + if (robotmodel.Id == RobotModelSelected.Id) + { + RobotModelPreviewRef.SetRobotPreview(null); + Table?.SetSelectedItem(null); + selectedRowNumber = -1; + } + RobotModels.Remove(robotmodel); + Table?.ReloadServerData(); + Snackbar.Add($"Xóa robot model thành công.", Severity.Success); + } + else Snackbar.Add($"Robot Model không tồn tại.", Severity.Warning); + } + StateHasChanged(); + } + } +} diff --git a/RobotNet.WebApp/Pages/RobotModelManager.razor.css b/RobotNet.WebApp/Pages/RobotModelManager.razor.css new file mode 100644 index 0000000..1da5b3b --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotModelManager.razor.css @@ -0,0 +1,21 @@ +.robot-preview { + width: 20%; + height: 100%; + border-left: 1px solid silver; +} + +.robot-create-item { + display: flex; + justify-content: center; + width: 100%; + height: 300px; +} + + .robot-create-item img { + justify-content: center; + width: 400px; + height: 300px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } diff --git a/RobotNet.WebApp/Pages/RobotMonitoring.razor b/RobotNet.WebApp/Pages/RobotMonitoring.razor new file mode 100644 index 0000000..447f3c4 --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotMonitoring.razor @@ -0,0 +1,251 @@ +@page "/robots/monitor" +@attribute [Authorize] +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares.Dtos +@using RobotNet.RobotShares.Dtos +@using RobotNet.RobotShares.Enums +@using RobotNet.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Robots.Components.Monitoring + +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpFactory +@inject RobotHubClient RobotHub + +Robot Monitoring + +
+
+ +
+ +
+
+
+ +
+ + + @IsDeactiveText + +
+ +@code { + + private MonitorMap MonitorMapRef = null!; + private MonitorToolbar MonitorToolbarRef = null!; + private MonitorInfomation MonitorInfomationRef = null!; + private MonitorToolBarModel ToolBarModel = new() + { + ShowGrid = false, + ShowName = true, + ShowPath = true, + FocusRobot = false, + ShowLaser = false, + ShowElement = true, + }; + + private List Maps = []; + private Guid MapIdSelected = new(); + + private bool IsDeactive = false; + private string IsDeactiveText = "Deactive"; + private RobotInfomationDto RobotSelected = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + RobotHub.MapDeactive += OnDeactive; + RobotHub.UpdateChanged += OnUpdateChanged; + RobotHub.ElementsStateChanged += ElementsStateChanged; + await RobotHub.StartAsync(); + + await LoadMaps(); + } + + private async Task LoadMaps() + { + try + { + IsDeactive = true; + IsDeactiveText = "IsLoading..."; + Maps.Clear(); + StateHasChanged(); + + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var maps = await Http.GetFromJsonAsync>($"api/MapsManager?txtSearch="); + + if (maps is null || maps.Count() == 0) + { + IsDeactiveText = "Map Empty"; + StateHasChanged(); + return; + } + + var mapsActive = maps.Where(m => m.Active).ToList(); + if (mapsActive.Count == 0) + { + IsDeactiveText = "Map Empty"; + StateHasChanged(); + return; + } + + Maps.AddRange(mapsActive ?? []); + + if (Maps.Count > 0) + { + MonitorToolbarRef.LoadMaps(Maps, Maps[0]); + await LoadMapData(Maps[0].Id); + } + + IsDeactive = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task LoadMapData(Guid Id) + { + try + { + IsDeactive = true; + IsDeactiveText = "IsLoading..."; + StateHasChanged(); + + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var mapDataResult = await Http.GetFromJsonAsync>($"api/MapsData/{Id}"); + if (mapDataResult is not null && mapDataResult.Data is not null) + { + await MonitorMapRef.LoadMap(null); + await MonitorMapRef.LoadMap(mapDataResult.Data); + MapIdSelected = mapDataResult.Data.Id; + IsDeactive = false; + if (RobotHub.IsConnected) await RobotHub.MapActive(MapIdSelected); + } + else IsDeactiveText = "Map Not Existed"; + + IsDeactive = false; + StateHasChanged(); + } + catch + { + IsDeactiveText = "Map Not Existed"; + StateHasChanged(); + return; + } + } + + private async Task ToolbarCheckedClick(MonitorToolbarCheckedType type, bool value) + { + switch (type) + { + case MonitorToolbarCheckedType.ShowGrid: + ToolBarModel.ShowGrid = value; + break; + case MonitorToolbarCheckedType.ShowName: + ToolBarModel.ShowName = value; + break; + case MonitorToolbarCheckedType.ShowPath: + ToolBarModel.ShowPath = value; + break; + case MonitorToolbarCheckedType.ShowLaser: + ToolBarModel.ShowLaser = value; + break; + case MonitorToolbarCheckedType.ShowElement: + ToolBarModel.ShowElement = value; + break; + case MonitorToolbarCheckedType.FocusRobot: + ToolBarModel.FocusRobot = value; + MonitorMapRef.FocusRobot(value); + if (value) await MonitorMapRef.FocusRobotAsync(); + break; + } + StateHasChanged(); + } + + private void ExpandChanged(bool value) => MonitorInfomationRef.ExpandedClick(value); + + private async Task ButtonEventClick(MonitorToolbarButtonType type) + { + switch (type) + { + case MonitorToolbarButtonType.Fit: + await MonitorMapRef.ScaleFitContentAsync(); + break; + case MonitorToolbarButtonType.Focus: + await MonitorMapRef.FocusRobotAsync(); + break; + case MonitorToolbarButtonType.ZoomIn: + await MonitorMapRef.ScaleZoom(0.5); + break; + case MonitorToolbarButtonType.ZoomOut: + await MonitorMapRef.ScaleZoom(-0.5); + break; + } + } + + private async Task MapChanged(MapInfoDto map) + { + if (map.Id != MapIdSelected) await LoadMapData(map.Id); + } + + private void RobotChanged(RobotInfomationDto robot) + { + if(robot.RobotId != RobotSelected.RobotId) + { + RobotSelected = robot; + MonitorMapRef.RobotSelectedChange(RobotSelected.RobotId); + } + } + + private bool IsUpdateing = false; + private void OnUpdateChanged(IEnumerable robotInfos) + { + if (IsUpdateing) return; + IsUpdateing = true; + InvokeAsync(async () => + { + var robots = robotInfos.Where(r => r.MapId == MapIdSelected).ToList(); + await MonitorMapRef.SetRobotPosition([.. robots]); + MonitorToolbarRef.LoadRobots([.. robotInfos]); + var robotSelect = robots.FirstOrDefault(r => r.RobotId == RobotSelected.RobotId); + if (robotSelect is not null) MonitorInfomationRef.UpdateState(robotSelect); + IsUpdateing = false; + }); + } + + private void ElementsStateChanged(IEnumerable elementsState) + { + _ = InvokeAsync(() => + { + var elements = elementsState.Where(e => e.MapId == MapIdSelected).ToList(); + MonitorMapRef.ElementStateUpdated([.. elements]); + }); + } + + private void OnDeactive() + { + IsDeactiveText = "Deactive"; + IsDeactive = true; + StateHasChanged(); + RobotHub.UpdateChanged -= OnUpdateChanged; + RobotHub.MapDeactive -= OnDeactive; + _ = RobotHub.StopAsync(); + } + + public async ValueTask DisposeAsync() + { + RobotHub.MapDeactive -= OnDeactive; + RobotHub.UpdateChanged -= OnUpdateChanged; + await RobotHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Pages/RobotTrafficManager.razor b/RobotNet.WebApp/Pages/RobotTrafficManager.razor new file mode 100644 index 0000000..82b7216 --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotTrafficManager.razor @@ -0,0 +1,431 @@ +@page "/traffic-manager" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares.Dtos +@using RobotNet.RobotShares.Dtos +@using RobotNet.RobotShares.Models +@using RobotNet.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Robots.Components +@using RobotNet.WebApp.Robots.Components.Traffic + +@inject ISnackbar Snackbar +@inject TrafficHubClient TrafficHub +@inject IDialogService Dialog +@inject IHttpClientFactory HttpFactory + +Traffic Manager + + + +
+
+
+ + + @foreach (var map in Maps) + { + @map.MapName + } + + @* + Add Lock + *@ +
+
+ + + Nr + RobotId + Traffic State + Target Node + Release Node + In Node + Locked Node + + + + + @(Table?.CurrentPage * Table?.RowsPerPage + AgentShow.IndexOf(context) + 1) + + + @context.RobotId + + + @context.State + + + @(context.Nodes.Count > 0 ? (string.IsNullOrEmpty(context.Nodes[^1].Name) ? context.Nodes[^1].Id.ToString("N").Substring(0, 4) : context.Nodes[^1].Name) : "") + + + @(context.ReleaseNode is null ? "" : string.IsNullOrEmpty(context.ReleaseNode.Name) ? context.ReleaseNode.Id.ToString("N").Substring(0, 4) : context.ReleaseNode.Name) + + + @(context.InNode is null ? "" : string.IsNullOrEmpty(context.InNode.Name) ? context.InNode.Id.ToString("N").Substring(0, 4) : context.InNode.Name) + + + @(context.LockedNodes.Count > 0 ? (string.IsNullOrEmpty(context.LockedNodes[^1].Name) ? context.LockedNodes[^1].Id.ToString("N").Substring(0, 4) : context.LockedNodes[^1].Name) : "") + + +
+ + Edit + Delete + +
+
+
+ +
+ +
+
+
+
+
+
+ +
+ + @DeactiveStr + +
+ + + + + Edit Agent @AgentUpdate.AgentId + + + + +
+ + + + No items found + + + + Add +
+ +
+ @foreach (var node in AgentUpdate.LockedNodes) + { +
+ + @(string.IsNullOrEmpty(node.Name) ? node.Id.ToString("N").Substring(0, 4) : node.Name) + +
+ } +
+
+
+ + Cancel + Update + +
+ +@code { + private string? txtSearch { get; set; } + private bool IsLoading { get; set; } + + private bool IsDeactive; + private string DeactiveStr = "Robot Empty"; + + private MudTable? Table; + private readonly List Agents = []; + private List AgentShow = []; + + private int selectedRowNumber = -1; + private TrafficAgentDto? AgentSelected; + + private List Maps = []; + private TrafficMapDto? MapSelected; + private TrafficAgentReview TrafficAgentReviewRef = default!; + + private bool IsEditAgentVisible; + private readonly UpdateAgentLockerModel AgentUpdate = new(); + private List Nodes = []; + private NodeDto? SelectedNode; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + TrafficHub.TrafficAgentUpdated += TrafficAgentUpdated; + TrafficHub.MapDeactive += MapDeactive; + await TrafficHub.StartAsync(); + await LoadTrafficMaps(); + } + + private async Task LoadTrafficMaps() + { + IsLoading = true; + StateHasChanged(); + + var trafficMaps = await TrafficHub.LoadTrafficMaps(); + if (!trafficMaps.IsSuccess) Snackbar.Add(trafficMaps.Message, Severity.Warning); + else if (trafficMaps.Data is null || !trafficMaps.Data.Any()) + { + Snackbar.Add("Không có bản đồ nào được tải lên", Severity.Warning); + IsLoading = false; + return; + } + else + { + Maps = [.. trafficMaps.Data]; + if (Maps.Count > 0) + { + MapSelected = Maps.First(); + Agents.Clear(); + Agents.AddRange(MapSelected.Agents); + Table?.ReloadServerData(); + await TrafficHub.TrafficActive(MapSelected.MapId); + } + } + + IsLoading = false; + StateHasChanged(); + } + + private void TextSearchChanged(string text) + { + txtSearch = text; + Table?.ReloadServerData(); + } + + private bool FilterFunc(TrafficAgentDto robot) + { + if (string.IsNullOrWhiteSpace(txtSearch)) + return true; + if (robot.RobotId.Contains(txtSearch, StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + private Task> ReloadData(TableState state, CancellationToken _) + { + AgentShow.Clear(); + var agents = new List(); + Agents.ForEach(agent => + { + if (FilterFunc(agent)) agents.Add(agent); + }); + AgentShow = agents.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); + return Task.FromResult(new TableData() { TotalItems = agents.Count(), Items = AgentShow }); + } + + private void RowClickEvent(TableRowClickEventArgs tableRowClickEventArgs) { } + + private string SelectedRowClassFunc(TrafficAgentDto element, int rowNumber) + { + if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && !Table.SelectedItem.Equals(element)) + { + return string.Empty; + } + else if (selectedRowNumber == rowNumber && Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + return "selected"; + } + else if (Table?.SelectedItem != null && Table.SelectedItem.Equals(element)) + { + selectedRowNumber = rowNumber; + AgentSelected = element; + TrafficAgentReviewRef.UpdateState(AgentSelected); + return "selected"; + } + else + { + return string.Empty; + } + } + + private async Task MapChanged() + { + if (MapSelected is not null) + { + var trafficMap = await TrafficHub.LoadTrafficMap(MapSelected.MapId); + if (!trafficMap.IsSuccess) Snackbar.Add(trafficMap.Message, Severity.Warning); + else if (trafficMap.Data is null) + { + Snackbar.Add("Không có agent nào trong bản đồ này", Severity.Warning); + Agents.Clear(); + AgentShow.Clear(); + if (Table is not null) await Table.ReloadServerData(); + } + else + { + Agents.Clear(); + Agents.AddRange(trafficMap.Data.Agents); + if (Table is not null) await Table.ReloadServerData(); + await TrafficHub.TrafficActive(MapSelected.MapId); + } + } + } + + private void TrafficAgentUpdated(IEnumerable agents) + { + bool isNewUpdate = false; + foreach (var newagent in agents) + { + var agent = Agents.FirstOrDefault(a => a.RobotId == newagent.RobotId); + if (agent is null) + { + isNewUpdate = true; + Agents.Add(newagent); + } + else + { + if ((agent.ReleaseNode is null && newagent.ReleaseNode is not null) || (agent.ReleaseNode is not null && newagent.ReleaseNode is null) || + (agent.ReleaseNode is not null && newagent.ReleaseNode is not null && agent.ReleaseNode.Id != newagent.ReleaseNode.Id) || + (agent.InNode is null && newagent.InNode is not null) || (agent.InNode is not null && newagent.InNode is null) || + (agent.InNode is not null && newagent.InNode is not null && agent.InNode.Id != newagent.InNode.Id) || + agent.State != newagent.State || + (agent.Nodes.Count > 0 && newagent.Nodes.Count == 0) || (agent.Nodes.Count == 0 && newagent.Nodes.Count > 0) || + (agent.Nodes.Count > 0 && newagent.Nodes.Count > 0 && agent.Nodes[^1].Id != newagent.Nodes[^1].Id) || + (agent.LockedNodes.Count > 0 && newagent.LockedNodes.Count == 0) || (agent.LockedNodes.Count == 0 && newagent.LockedNodes.Count > 0) || + (agent.LockedNodes.Count > 0 && newagent.LockedNodes.Count > 0 && agent.LockedNodes[^1].Id != newagent.LockedNodes[^1].Id) + ) isNewUpdate = true; + agent.Nodes = [.. newagent.Nodes]; + agent.ReleaseNode = newagent.ReleaseNode; + agent.LockedNodes = [.. newagent.LockedNodes]; + agent.InNode = newagent.InNode; + agent.State = newagent.State; + agent.SubNodes = [.. newagent.SubNodes]; + agent.GiveWayNodes = [.. newagent.GiveWayNodes]; + agent.ConflictNode = newagent.ConflictNode; + agent.ConflictAgentId = newagent.ConflictAgentId; + } + if (AgentSelected is not null && newagent.RobotId == AgentSelected.RobotId) TrafficAgentReviewRef.UpdateState(AgentSelected); + } + if (isNewUpdate) Table?.ReloadServerData(); + } + + private void MapDeactive() + { + DeactiveStr = "Deactive"; + IsDeactive = true; + StateHasChanged(); + TrafficHub.TrafficAgentUpdated -= TrafficAgentUpdated; + TrafficHub.MapDeactive -= MapDeactive; + _ = TrafficHub.StopAsync(); + } + + private async Task DeleteAgent(TrafficAgentDto agent) + { + var parameters = new DialogParameters + { + { x => x.Content, "Bạn có chắc chắn muốn xóa agent đi không?" }, + { x => x.ConfirmText, "Delete" }, + { x => x.Color, Color.Secondary } + }; + var ConfirmDelete = await Dialog.ShowAsync("Xoá Agent", parameters); + var result = await ConfirmDelete.Result; + if (result is not null && result.Data is not null && bool.TryParse(result.Data.ToString(), out bool data) && data) + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var delete = await Http.DeleteFromJsonAsync($"api/TrafficManager/{MapSelected?.MapId}/{agent.RobotId}"); + if (delete is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!delete.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {delete.Message}", Severity.Error); + else + { + Agents.Remove(agent); + Snackbar.Add("Xóa robot thành công!", Severity.Success); + Table?.ReloadServerData(); + } + StateHasChanged(); + } + } + + private async Task OpenUpdateAgent(TrafficAgentDto agent) + { + AgentUpdate.AgentId = agent.RobotId; + AgentUpdate.LockedNodes = agent.LockedNodes; + AgentUpdate.MapId = MapSelected is null ? Guid.Empty : MapSelected.MapId; + await LoadNodes(); + IsEditAgentVisible = true; + StateHasChanged(); + } + + private async Task UpdateAgent() + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var result = await (await Http.PutAsJsonAsync($"api/TrafficManager", AgentUpdate)).Content.ReadFromJsonAsync(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else Snackbar.Add("Cập nhật thành công", Severity.Success); + + IsEditAgentVisible = false; + StateHasChanged(); + } + + public async Task LoadNodes() + { + try + { + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var result = await Http.GetFromJsonAsync($"api/Nodes/{MapSelected?.MapId}"); + if (result is not null) Nodes = result.Where(n => !string.IsNullOrEmpty(n.Name)).ToList(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task> NodeSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Nodes.Select(n => n); + } + + return Nodes.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(n => n); + } + + private void NodeChanged(NodeDto value) + { + if (SelectedNode is null || value.Id != SelectedNode.Id) SelectedNode = value; + } + + private void AddNode() + { + if (SelectedNode is not null && !AgentUpdate.LockedNodes.Any(n => n.Id == SelectedNode.Id)) AgentUpdate.LockedNodes.Add(new() + { + Id = SelectedNode.Id, + X = SelectedNode.X, + Y = SelectedNode.Y, + Name = SelectedNode.Name, + }); + StateHasChanged(); + } + + public async ValueTask DisposeAsync() + { + TrafficHub.TrafficAgentUpdated -= TrafficAgentUpdated; + TrafficHub.MapDeactive -= MapDeactive; + await TrafficHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Pages/RobotTrafficManager.razor.css b/RobotNet.WebApp/Pages/RobotTrafficManager.razor.css new file mode 100644 index 0000000..652a066 --- /dev/null +++ b/RobotNet.WebApp/Pages/RobotTrafficManager.razor.css @@ -0,0 +1,19 @@ +.agent-preview { + width: 30%; + height: 100%; + border-left: 1px solid silver; +} + +.paper-nodes { + padding: 5px; + border: 1px solid silver; + border-radius: 10px; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + max-width: 530px; + height: 200px; +} diff --git a/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor new file mode 100644 index 0000000..75f4792 --- /dev/null +++ b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor @@ -0,0 +1,602 @@ +@layout Layout.HMILayout +@page "/sehc/da3/line1" + +@using Microsoft.AspNetCore.SignalR.Client +@using RobotNet.Script.Shares +@using RobotNet.WebApp.Clients + +@attribute [Authorize] +@implements IAsyncDisposable + +@inject HMIHubClient hmiClient +@inject ISnackbar Snackbar +@inject IDialogService Dialog + +
+
+
+ +
+
+ STEP NUMBER +
+
+
+ @Line1_HMI_Model.SEHCMissionStep +
+
+ +
+
+ +
+
+
+
+ + + + + + + + +
+ Trolley +
+
+ +
+
+ + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+ CONNECTING ... +
+
+
+ + +
+
+ +
+
+ WAIT FOR RUNNING ... +
+
+
+ +@code { + private bool DisableControl = true; + private bool DisableProcess => !DisableControl && processorState != ProcessorState.Running; + private ProcessorState processorState = ProcessorState.Idle; + + private string hmiBtnResetStepClass => $"{(Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1 ? "inactive" : "")}"; + private string hmiBtnIsWorkingClass => $"{(Line1_HMI_Model.IsWorking ? "" : "inactive")}"; + private string hmiBtnHasEmptyTrolleyClass => $"{(Line1_HMI_Model.Has_Empty_Trolley ? "" : "inactive")}"; + private string hmiBtnGoHomeClass => $"{(Line1_HMI_Model.Request_GoHome ? "inactive" : "")}"; + + private string hmiBtnEnAutoAClass => $"{(Line1_HMI_Model.EnableAutoA ? "" : "off")}"; + private string hmiBtnInAClass => $"{(Line1_HMI_Model.EnableAutoA ? "inactive" : "")} {(Line1_HMI_Model.Req_In_A ? "" : "off")}"; + private string hmiBtnInBesideAClass => $"{(Line1_HMI_Model.EnableAutoA ? "inactive" : "")} {(Line1_HMI_Model.Req_In_Beside_A ? "" : "off")}"; + private string hmiBtnCloseAClass => $"{(Line1_HMI_Model.EnableAutoA ? "inactive" : "")} {(Line1_HMI_Model.Req_Close_Cylinder_A ? "" : "off")}"; + + private string hmiBtnEnAutoBClass => $"{(Line1_HMI_Model.EnableAutoB ? "" : "off")}"; + private string hmiBtnInBClass => $"{(Line1_HMI_Model.EnableAutoB ? "inactive" : "")} {(Line1_HMI_Model.Req_In_B ? "" : "off")}"; + private string hmiBtnInBesideBClass => $"{(Line1_HMI_Model.EnableAutoB ? "inactive" : "")} {(Line1_HMI_Model.Req_In_Beside_B ? "" : "off")}"; + private string hmiBtnCloseBClass => $"{(Line1_HMI_Model.EnableAutoB ? "inactive" : "")} {(Line1_HMI_Model.Req_Close_Cylinder_B ? "" : "off")}"; + + private string cylinderStateAClass => $"{(Line1_HMI_Model.Cylinder_Opened_A ? "" : "led-indicator")}"; + private string cylinderStateBClass => $"{(Line1_HMI_Model.Cylinder_Opened_B ? "" : "led-indicator")}"; + + private string trolleyAClass => $"{(Line1_HMI_Model.Empty_A ? "trolley-inactive" : "")}"; + private string trolleyBClass => $"{(Line1_HMI_Model.Empty_B ? "trolley-inactive" : "")}"; + private string trolleyFullClass => $"{(Line1_HMI_Model.AGV_Push_Noti_Mem ? "" : "trolley-inactive")}"; + + private string indicatorAClass => GetIndicatorAClass(); + private string indicatorBClass => GetIndicatorBClass(); + + private string amrReadyUndockAClass => $"{(Line1_HMI_Model.AMR_Ready_Undock_A ? "" : "image-inactive")}"; + private string amrReadyUndockBClass => $"{(Line1_HMI_Model.AMR_Ready_Undock_B ? "" : "image-inactive")}"; + + private string amrInAClass => $"{(Line1_HMI_Model.AMR_In_Station_A ? "" : "image-inactive")}"; + private string amrInBClass => $"{(Line1_HMI_Model.AMR_In_Station_B ? "" : "image-inactive")}"; + + private string accessInAClass => Line1_HMI_Model.Access_In_A ? "access-allowed" : "access-denied"; + private string accessInBClass => Line1_HMI_Model.Access_In_B ? "access-allowed" : "access-denied"; + private string accessInBesideAClass => Line1_HMI_Model.Access_In_Beside_A ? "access-allowed" : "access-denied"; + private string accessInBesideBClass => Line1_HMI_Model.Access_In_Beside_B ? "access-allowed" : "access-denied"; + + private SEHC_DA3_Line1_HMI_Model Line1_HMI_Model; + + IDictionary SyncVariables = new Dictionary() + { + {nameof(SEHC_DA3_Line1_HMI_Model.IsWorking), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.SEHCMissionStep), "-1" }, + {nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoA), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoB), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.AMR_In_Station_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.AMR_In_Station_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.AMR_Ready_Undock_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.AMR_Ready_Undock_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_In_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_In_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Robot_Working_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Robot_Working_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Empty_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Empty_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Cylinder_Opened_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Cylinder_Opened_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Access_In_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Access_In_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Access_In_Beside_A), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Access_In_Beside_B), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.AGV_Push_Noti_Mem), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Has_Empty_Trolley), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyInStationA), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyInStationB), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyOutStationA), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyOutStationB), bool.FalseString }, + {nameof(SEHC_DA3_Line1_HMI_Model.Request_GoHome), bool.FalseString }, + }; + + System.Timers.Timer timer = new System.Timers.Timer(300) + { + AutoReset = true, + }; + + protected override async Task OnInitializedAsync() + { + timer.Elapsed += OnTimerElapsed; + + hmiClient.ConnectionStateChanged += OnConnectionStateChanged; + hmiClient.StateChanged += OnProcessStateChanged; + + await hmiClient.StartAsync(); + + timer.Start(); + OnProcessStateChanged(await hmiClient.GetState()); + + await base.OnInitializedAsync(); + } + + void OnConnectionStateChanged(HubConnectionState state) + { + Console.WriteLine($"HubConnectionState={state}"); + DisableControl = state != HubConnectionState.Connected; + StateHasChanged(); + } + + void OnProcessStateChanged(ProcessorState state) + { + processorState = state; + if (state == ProcessorState.Running) + { + if (!timer.Enabled) + { + timer.Start(); + } + } + else if (timer.Enabled) + { + timer.Stop(); + } + StateHasChanged(); + } + + void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + InvokeAsync(async Task () => + { + var variables = await hmiClient.GetVariables(SyncVariables.Keys); + foreach (var key in variables.Keys) + { + SyncVariables[key] = variables[key]; + } + + Line1_HMI_Model.IsWorking = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.IsWorking)]); + Line1_HMI_Model.SEHCMissionStep = int.TryParse(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.SEHCMissionStep)], out int step) ? step : -1; + Line1_HMI_Model.EnableAutoA = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoA)]); + Line1_HMI_Model.EnableAutoB = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoB)]); + Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1 = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1)]); + Line1_HMI_Model.AMR_In_Station_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.AMR_In_Station_A)]); + Line1_HMI_Model.AMR_In_Station_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.AMR_In_Station_B)]); + Line1_HMI_Model.AMR_Ready_Undock_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.AMR_Ready_Undock_A)]); + Line1_HMI_Model.AMR_Ready_Undock_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.AMR_Ready_Undock_B)]); + Line1_HMI_Model.Req_In_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_In_A)]); + Line1_HMI_Model.Req_In_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_In_B)]); + Line1_HMI_Model.Req_In_Beside_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_A)]); + Line1_HMI_Model.Req_In_Beside_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_B)]); + Line1_HMI_Model.Req_Close_Cylinder_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_A)]); + Line1_HMI_Model.Req_Close_Cylinder_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_B)]); + Line1_HMI_Model.Robot_Working_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Robot_Working_A)]); + Line1_HMI_Model.Robot_Working_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Robot_Working_B)]); + Line1_HMI_Model.Empty_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Empty_A)]); + Line1_HMI_Model.Empty_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Empty_B)]); + Line1_HMI_Model.Cylinder_Opened_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Cylinder_Opened_A)]); + Line1_HMI_Model.Cylinder_Opened_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Cylinder_Opened_B)]); + Line1_HMI_Model.Access_In_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Access_In_A)]); + Line1_HMI_Model.Access_In_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Access_In_B)]); + Line1_HMI_Model.Access_In_Beside_A = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Access_In_Beside_A)]); + Line1_HMI_Model.Access_In_Beside_B = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Access_In_Beside_B)]); + Line1_HMI_Model.AGV_Push_Noti_Mem = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.AGV_Push_Noti_Mem)]); + Line1_HMI_Model.Has_Empty_Trolley = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Has_Empty_Trolley)]); + Line1_HMI_Model.MoveTrolleyInStationA = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyInStationA)]); + Line1_HMI_Model.MoveTrolleyInStationB = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyInStationB)]); + Line1_HMI_Model.MoveTrolleyOutStationA = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyOutStationA)]); + Line1_HMI_Model.MoveTrolleyOutStationB = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.MoveTrolleyOutStationB)]); + Line1_HMI_Model.Request_GoHome = bool.TrueString.Equals(SyncVariables[nameof(SEHC_DA3_Line1_HMI_Model.Request_GoHome)]); + StateHasChanged(); + }).ConfigureAwait(false); + } + + string GetIndicatorAClass() + { + if (Line1_HMI_Model.Robot_Working_A) return "working"; + + if (Line1_HMI_Model.IsWorking) + { + if (Line1_HMI_Model.MoveTrolleyOutStationA) + { + return "request-out"; + } + else if (Line1_HMI_Model.MoveTrolleyInStationA) + { + return "request-in"; + } + else + { + return "standby"; + } + } + else + { + return "hidden"; + } + } + + string GetIndicatorBClass() + { + if (Line1_HMI_Model.Robot_Working_B) return "working"; + + if (Line1_HMI_Model.IsWorking) + { + if (Line1_HMI_Model.MoveTrolleyOutStationB) + { + return "request-out"; + } + else if (Line1_HMI_Model.MoveTrolleyInStationB) + { + return "request-in"; + } + else + { + return "standby"; + } + } + else + { + return "hidden"; + } + } + + async Task ToggleAutoManualA() + { + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoA), Line1_HMI_Model.EnableAutoA ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + async Task ToggleAutoManualB() + { + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.EnableAutoB), Line1_HMI_Model.EnableAutoB ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqInA() + { + if (Line1_HMI_Model.EnableAutoA) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_In_A), Line1_HMI_Model.Req_In_A ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqInBesideA() + { + if (Line1_HMI_Model.EnableAutoA) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_A), Line1_HMI_Model.Req_In_Beside_A ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqCloseCylenderA() + { + if (Line1_HMI_Model.EnableAutoA) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_A), Line1_HMI_Model.Req_Close_Cylinder_A ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqInB() + { + if (Line1_HMI_Model.EnableAutoB) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_In_B), Line1_HMI_Model.Req_In_B ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqInBesideB() + { + if (Line1_HMI_Model.EnableAutoB) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_In_Beside_B), Line1_HMI_Model.Req_In_Beside_B ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleReqCloseCylenderB() + { + if (Line1_HMI_Model.EnableAutoB) return; + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Req_Close_Cylinder_B), Line1_HMI_Model.Req_Close_Cylinder_B ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleHasEmptyTrolley() + { + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Has_Empty_Trolley), Line1_HMI_Model.Has_Empty_Trolley ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleGoHome() + { + if (Line1_HMI_Model.Request_GoHome) return; + + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Request_GoHome), bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + async Task ToggleHasFullTrolley() + { + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.AGV_Push_Noti_Mem), Line1_HMI_Model.AGV_Push_Noti_Mem ? bool.FalseString : bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + async Task CallResetStep() + { + if (Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1) return; + bool? confirm = await Dialog.ShowMessageBox( + "Reset Mission State", + "Hãy xác nhận đặt lại trạng thái hoạt động!", + yesText: "Yes", cancelText: "Cancel"); + + if (confirm != true) return; + + var result = await hmiClient.SetVariable(nameof(SEHC_DA3_Line1_HMI_Model.Request_Reset_SEHC_DA3_Line1), bool.TrueString); + if (result == null) + { + Snackbar.Add("HMI client SetVariable failed to receiver response from server", Severity.Error); + } + else if (!result.IsSuccess) + { + Snackbar.Add($"HMI client SetVariable error: {result.Message}", Severity.Error); + } + } + + public async ValueTask DisposeAsync() + { + timer.Stop(); + + hmiClient.ConnectionStateChanged -= OnConnectionStateChanged; + hmiClient.StateChanged -= OnProcessStateChanged; + + await hmiClient.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.cs b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.cs new file mode 100644 index 0000000..ff34522 --- /dev/null +++ b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.cs @@ -0,0 +1,37 @@ +namespace RobotNet.WebApp.Pages; + +public struct SEHC_DA3_Line1_HMI_Model +{ + public bool IsWorking; + public int SEHCMissionStep; + public bool EnableAutoA; + public bool EnableAutoB; + public bool Request_Reset_SEHC_DA3_Line1; + public bool AMR_In_Station_A; + public bool AMR_In_Station_B; + public bool AMR_Ready_Undock_A; + public bool AMR_Ready_Undock_B; + public bool Req_In_A; + public bool Req_In_B; + public bool Req_In_Beside_A; + public bool Req_In_Beside_B; + public bool Req_Close_Cylinder_A; + public bool Req_Close_Cylinder_B; + public bool Robot_Working_A; + public bool Robot_Working_B; + public bool Empty_A; + public bool Empty_B; + public bool Cylinder_Opened_A; + public bool Cylinder_Opened_B; + public bool Access_In_A; + public bool Access_In_B; + public bool Access_In_Beside_A; + public bool Access_In_Beside_B; + public bool AGV_Push_Noti_Mem; + public bool Has_Empty_Trolley; + public bool MoveTrolleyInStationA; + public bool MoveTrolleyInStationB; + public bool MoveTrolleyOutStationA; + public bool MoveTrolleyOutStationB; + public bool Request_GoHome; +} diff --git a/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.css b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.css new file mode 100644 index 0000000..c60c8dc --- /dev/null +++ b/RobotNet.WebApp/Pages/SEHC_DA3_Line1_HMI.razor.css @@ -0,0 +1,554 @@ +/* HMI Button Styles */ +.hmi-button { + background: linear-gradient(145deg, #e6e6e6, #cccccc); + border: 2px solid #999999; + border-radius: 8px; + color: #333333; + font-weight: bold; + font-size: 18px; + padding: 0 16px; + margin: 0 4px; + min-width: 120px; + height: 80%; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.3), inset 1px 1px 3px rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8); + position: relative; + user-select: none; + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; +} + + .hmi-button:hover { + background: linear-gradient(145deg, #f0f0f0, #d6d6d6); + transform: translateY(-1px); + } + + .hmi-button:active { + background: linear-gradient(145deg, #cccccc, #b3b3b3); + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3), inset 2px 2px 4px rgba(0, 0, 0, 0.2); + transform: translateY(1px); + } + + .hmi-button.start { + background: linear-gradient(145deg, #4CAF50, #45a049); + color: white; + border-color: #2e7d32; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); + } + + .hmi-button.start:hover { + background: linear-gradient(145deg, #5cbf60, #4fa152); + } + + .hmi-button.request { + background: linear-gradient(145deg, #2196F3, #1976D2); + color: white; + border-color: #0d47a1; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); + } + + .hmi-button.request:hover { + background: linear-gradient(145deg, #42a5f5, #1e88e5); + } + + .hmi-button.close { + background: linear-gradient(145deg, #FF9800, #F57C00); + color: white; + border-color: #bf360c; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); + } + + .hmi-button.close:hover { + background: linear-gradient(145deg, #ffb74d, #fb8c00); + } + + .hmi-button.onoff { + background: linear-gradient(145deg, #2196F3, #1976D2); + color: white; + border-radius: 8px; + font-size: 14px; + padding: 18px 8px; + height: 60px; + font-weight: bold; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.3), inset 1px 1px 3px rgba(255, 255, 255, 0.2); + transition: all 0.2s ease; + cursor: pointer; + font-family: 'Courier New', monospace; + } + + .hmi-button.onoff:hover { + background: linear-gradient(145deg, #42a5f5, #1e88e5); + } + + .hmi-button.off { + filter: grayscale(100%); + } +/* Station Direction Indicator Styles */ +.station-indicator { + display: flex; + align-items: center; + justify-content: center; + width: 90%; + height: 80%; + margin: 0 auto; + position: relative; + background: transparent; + border: none; + overflow: hidden; +} + +.direction-track { + display: flex; + align-items: center; + width: 100%; + height: 100%; + position: relative; + padding: 0 15px; +} + +.arrow-flow { + position: absolute; + width: 20px; + height: 20px; + opacity: 0; + transition: all 0.3s ease; +} + + .arrow-flow::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 0; + height: 0; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-bottom: 12px solid #00ff00; + transform: translate(-50%, -50%) rotate(90deg); + } + +/* Station States - COPIED EXACTLY FROM WORKING VERSION */ +.station-indicator.request-in { + border-color: transparent; + box-shadow: none; + animation: none; +} + + .station-indicator.request-in .arrow-flow { + animation: flow-in-left 1.2s infinite linear; + } + + .station-indicator.request-in .arrow-flow:nth-child(1), + .station-indicator.request-in .arrow-flow:nth-child(3), + .station-indicator.request-in .arrow-flow:nth-child(5), + .station-indicator.request-in .arrow-flow:nth-child(7) { + animation: flow-in-left 0.8s infinite linear; + } + + .station-indicator.request-in .arrow-flow:nth-child(2), + .station-indicator.request-in .arrow-flow:nth-child(4), + .station-indicator.request-in .arrow-flow:nth-child(6), + .station-indicator.request-in .arrow-flow:nth-child(8) { + animation: flow-in-right 0.8s infinite linear; + } + + /* Mũi tên bên trái hướng về phải (→) */ + .station-indicator.request-in .arrow-flow:nth-child(1)::before, + .station-indicator.request-in .arrow-flow:nth-child(3)::before, + .station-indicator.request-in .arrow-flow:nth-child(5)::before, + .station-indicator.request-in .arrow-flow:nth-child(7)::before { + transform: translate(-50%, -50%) rotate(90deg); + } + + /* Mũi tên bên phải hướng về trái (←) */ + .station-indicator.request-in .arrow-flow:nth-child(2)::before, + .station-indicator.request-in .arrow-flow:nth-child(4)::before, + .station-indicator.request-in .arrow-flow:nth-child(6)::before, + .station-indicator.request-in .arrow-flow:nth-child(8)::before { + transform: translate(-50%, -50%) rotate(-90deg); + } + + .station-indicator.request-in .arrow-flow::before { + border-bottom-color: #00ff00; + color: #00ff00; + } + +.station-indicator.request-out { + border-color: transparent; + box-shadow: none; + animation: none; +} + + .station-indicator.request-out .arrow-flow { + animation: flow-out-left 1.2s infinite linear; + } + + .station-indicator.request-out .arrow-flow:nth-child(1), + .station-indicator.request-out .arrow-flow:nth-child(3), + .station-indicator.request-out .arrow-flow:nth-child(5), + .station-indicator.request-out .arrow-flow:nth-child(7) { + animation: flow-out-left 0.8s infinite linear; + } + + .station-indicator.request-out .arrow-flow:nth-child(2), + .station-indicator.request-out .arrow-flow:nth-child(4), + .station-indicator.request-out .arrow-flow:nth-child(6), + .station-indicator.request-out .arrow-flow:nth-child(8) { + animation: flow-out-right 0.8s infinite linear; + } + + /* Mũi tên bên trái hướng ra trái (←) */ + .station-indicator.request-out .arrow-flow:nth-child(1)::before, + .station-indicator.request-out .arrow-flow:nth-child(3)::before, + .station-indicator.request-out .arrow-flow:nth-child(5)::before, + .station-indicator.request-out .arrow-flow:nth-child(7)::before { + transform: translate(-50%, -50%) rotate(-90deg); + } + + /* Mũi tên bên phải hướng ra phải (→) */ + .station-indicator.request-out .arrow-flow:nth-child(2)::before, + .station-indicator.request-out .arrow-flow:nth-child(4)::before, + .station-indicator.request-out .arrow-flow:nth-child(6)::before, + .station-indicator.request-out .arrow-flow:nth-child(8)::before { + transform: translate(-50%, -50%) rotate(90deg); + } + + .station-indicator.request-out .arrow-flow::before { + border-bottom-color: #ff6600; + color: #ff6600; + transform: translate(-50%, -50%) rotate(-90deg); + } + +.station-indicator.standby { + border-color: transparent; + animation: none; +} + + .station-indicator.standby .center-dot { + background: radial-gradient(circle, #333 30%, #555 70%); + animation: none; + } + +.station-indicator.hidden { + opacity: 0; + transform: scale(0.8); + filter: blur(2px); +} + +/* Working state for station indicator */ +.station-indicator.working { + border-color: transparent; + box-shadow: none; + animation: none; + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; +} + + .station-indicator.working .direction-track { + display: none; + } + + .station-indicator.working::after { + content: 'WORKING'; + color: #27ae60; + font-weight: bold; + font-size: 20px; + font-family: 'Courier New', monospace; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); + letter-spacing: 2px; + animation: working-pulse 1.5s infinite ease-in-out; + } + +@keyframes working-pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.7; + transform: scale(1.05); + } +} + +/* Center Station Dot */ +.center-dot { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border-radius: 50%; + background: radial-gradient(circle, #333 30%, #555 70%); + border: 2px solid #666; + box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.1), 0 2px 8px rgba(0, 0, 0, 0.3); + z-index: 10; +} + +/* Flow from both sides to center (IN) */ +@keyframes flow-in-left { + 0% { + left: -25px; + opacity: 0; + transform: scale(0.5); + } + + 20% { + opacity: 1; + transform: scale(1); + } + + 80% { + opacity: 1; + transform: scale(1); + } + + 100% { + left: calc(50% - 15px); + opacity: 0; + transform: scale(1.2); + } +} + +@keyframes flow-in-right { + 0% { + right: -25px; + opacity: 0; + transform: scale(0.5); + } + + 20% { + opacity: 1; + transform: scale(1); + } + + 80% { + opacity: 1; + transform: scale(1); + } + + 100% { + right: calc(50% - 15px); + opacity: 0; + transform: scale(1.2); + } +} + +/* Flow from center to both sides (OUT) */ +@keyframes flow-out-left { + 0% { + left: calc(50% - 15px); + opacity: 0; + transform: scale(1.2); + } + + 20% { + opacity: 1; + transform: scale(1); + } + + 80% { + opacity: 1; + transform: scale(1); + } + + 100% { + left: -25px; + opacity: 0; + transform: scale(0.5); + } +} + +@keyframes flow-out-right { + 0% { + right: calc(50% - 15px); + opacity: 0; + transform: scale(1.2); + } + + 20% { + opacity: 1; + transform: scale(1); + } + + 80% { + opacity: 1; + transform: scale(1); + } + + 100% { + right: -25px; + opacity: 0; + transform: scale(0.5); + } +} + +/* Multiple Arrow Generation - COPIED EXACTLY FROM WORKING VERSION */ +.station-indicator.request-in .arrow-flow:nth-child(1) { + animation-delay: 0s; +} + +.station-indicator.request-in .arrow-flow:nth-child(2) { + animation-delay: 0.05s; +} + +.station-indicator.request-in .arrow-flow:nth-child(3) { + animation-delay: 0.2s; +} + +.station-indicator.request-in .arrow-flow:nth-child(4) { + animation-delay: 0.25s; +} + +.station-indicator.request-in .arrow-flow:nth-child(5) { + animation-delay: 0.4s; +} + +.station-indicator.request-in .arrow-flow:nth-child(6) { + animation-delay: 0.45s; +} + +.station-indicator.request-in .arrow-flow:nth-child(7) { + animation-delay: 0.6s; +} + +.station-indicator.request-in .arrow-flow:nth-child(8) { + animation-delay: 0.65s; +} + +.station-indicator.request-out .arrow-flow:nth-child(1) { + animation-delay: 0s; +} + +.station-indicator.request-out .arrow-flow:nth-child(2) { + animation-delay: 0.05s; +} + +.station-indicator.request-out .arrow-flow:nth-child(3) { + animation-delay: 0.2s; +} + +.station-indicator.request-out .arrow-flow:nth-child(4) { + animation-delay: 0.25s; +} + +.station-indicator.request-out .arrow-flow:nth-child(5) { + animation-delay: 0.4s; +} + +.station-indicator.request-out .arrow-flow:nth-child(6) { + animation-delay: 0.45s; +} + +.station-indicator.request-out .arrow-flow:nth-child(7) { + animation-delay: 0.6s; +} + +.station-indicator.request-out .arrow-flow:nth-child(8) { + animation-delay: 0.65s; +} + +/* Inactive State for Images */ +.image-inactive { + opacity: 0.3; + filter: grayscale(100%) brightness(0.7); + transition: all 0.3s ease; +} + + .image-inactive:hover { + opacity: 0.5; + filter: grayscale(80%) brightness(0.8); + } + +.inactive { + opacity: 0.3; + filter: grayscale(100%) brightness(0.7); + transition: all 0.3s ease; +} + + .inactive:hover { + opacity: 0.5; + filter: grayscale(80%) brightness(0.8); + } + +/* Specific inactive states for AMR and Trolley */ +.amr-inactive { + opacity: 0.25; + filter: grayscale(100%) brightness(0.6) contrast(0.8); + transition: all 0.4s ease; + border-color: #666 !important; + background-color: rgba(120, 120, 120, 0.1) !important; +} + +.trolley-inactive { + opacity: 0.3; + filter: grayscale(100%) brightness(0.7) contrast(0.9); + transition: all 0.4s ease; + border-color: #666 !important; + background-color: rgba(120, 120, 120, 0.1) !important; +} + + /* Inactive hover effects */ + .amr-inactive:hover, + .trolley-inactive:hover { + opacity: 0.4; + filter: grayscale(90%) brightness(0.8); + } + +/* LED indicators with turning on effect */ +.led-indicator { + width: 5.5%; + height: 7.5%; + background: linear-gradient(145deg, #27ae60, #2ecc71); + border: 2px solid #1e8449; + box-shadow: inset 0 1px 3px rgba(255, 255, 255, 0.3), 0 0 10px rgba(39, 174, 96, 0.8), 0 0 20px rgba(39, 174, 96, 0.4); + animation: turning-on 2s infinite ease-in-out; +} + +@keyframes turning-on { + 0%, 100% { + opacity: 1; + box-shadow: inset 0 1px 3px rgba(255, 255, 255, 0.3), 0 0 10px rgba(39, 174, 96, 0.8), 0 0 20px rgba(39, 174, 96, 0.4); + transform: scale(1); + } + + 50% { + opacity: 0.6; + box-shadow: inset 0 1px 3px rgba(255, 255, 255, 0.1), 0 0 5px rgba(39, 174, 96, 0.4), 0 0 10px rgba(39, 174, 96, 0.2); + transform: scale(0.95); + } +} + + +/* Access Status Indicator */ +.access-status { + transition: all 0.3s ease; +} + + .access-status.access-allowed { + background-color: #28a745; + /*border: 2px solid #1e7e34;*/ + } + + .access-status.access-denied { + background-color: #dc3545; + /*border: 2px solid #bd2130;*/ + } + + .access-status.access-allowed::before { + content: "✓"; + } + + .access-status.access-denied::before { + content: "✗"; + } \ No newline at end of file diff --git a/RobotNet.WebApp/Pages/ScriptEditor.razor b/RobotNet.WebApp/Pages/ScriptEditor.razor new file mode 100644 index 0000000..5808a60 --- /dev/null +++ b/RobotNet.WebApp/Pages/ScriptEditor.razor @@ -0,0 +1,121 @@ +@page "/script-editor" +@attribute [Authorize] +@implements IAsyncDisposable + +@using RobotNet.Script.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Scripts.Components +@using RobotNet.WebApp.Scripts.Models + +@inject IHttpClientFactory httpFactory +@inject ProcessorHubClient processorHub + +Script Editor + + +
+
+ + +
+
+
+ +
+
+ +
+
+
+
+ + +@code { + private EditorHierachy hierachyRef = default!; + private Editor editorRef = null!; + private ProcessorControl processorControlRef = null!; + + private int HierachyWidth = 300; + private int ConsoleHeight = 300; + private bool isDraggingHierachy = false; + private bool isDraggingConsole = false; + private ScriptWorkspace Workspace = default!; + + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + Workspace = new(httpFactory); + await Workspace.InitializeAsync(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if(firstRender) + { + processorHub.StateChanged += OnProcessStateChanged; + await processorHub.StartAsync(); + + OnProcessStateChanged(await processorHub.GetState()); + } + } + + + private void OnProcessStateChanged(ProcessorState state) + { + Workspace.ProcessorState = state; + editorRef.OnProcessStateChanged(state); + } + + private void EnableResizeHierachy() + { + isDraggingHierachy = true; + } + + private void EnableResizeConsole() + { + isDraggingConsole = true; + } + + private void DisableResize() + { + isDraggingHierachy = false; + isDraggingConsole = false; + } + + private void MouseMoveOnEditorContainer(MouseEventArgs e) + { + if (isDraggingHierachy) + { + HierachyWidth += (int)e.MovementX; + + if (HierachyWidth < 150) + { + HierachyWidth = 150; + } + else if (HierachyWidth > 500) + { + HierachyWidth = 500; + } + } + else if (isDraggingConsole) + { + // ConsoleHeight -= (int)e.MovementY; + // StateHasChanged(); + // Console.WriteLine($"ConsoleHeight={ConsoleHeight}"); + } + } + + public async ValueTask DisposeAsync() + { + processorHub.StateChanged -= OnProcessStateChanged; + await processorHub.StopAsync(); + + Workspace.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Pages/ScriptEditor.razor.css b/RobotNet.WebApp/Pages/ScriptEditor.razor.css new file mode 100644 index 0000000..3880ff1 --- /dev/null +++ b/RobotNet.WebApp/Pages/ScriptEditor.razor.css @@ -0,0 +1,62 @@ +.editor-container { + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + flex-direction: row; +} + +.split-vertical-view { + height: 100%; + transition-duration: 500ms; + transition-property: background-color; + background-color: transparent; + border-right: 1px solid grey; + width: 5px; +} + + .split-vertical-view:hover { + background-color: dodgerblue; + border-right: 1px solid dodgerblue; + cursor: col-resize; + } + +.editor-hierachy { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.editor-content { + height: 100%; + flex-grow: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.editor-code { + width: 100%; + flex-grow: 1; + overflow: hidden; +} + +.split-horizontal-view { + width: 100%; + transition-duration: 500ms; + transition-property: background-color; + background-color: transparent; + border-top: 1px solid grey; + height: 5px; +} + + .split-horizontal-view:hover { + background-color: dodgerblue; + border-right: 1px solid dodgerblue; + cursor: row-resize; + } + +.editor-console { + width: 100%; +} diff --git a/RobotNet.WebApp/Pages/ScriptManager.razor b/RobotNet.WebApp/Pages/ScriptManager.razor new file mode 100644 index 0000000..1f0dae7 --- /dev/null +++ b/RobotNet.WebApp/Pages/ScriptManager.razor @@ -0,0 +1,38 @@ +@page "/script-manager" +@attribute [Authorize] +@implements IAsyncDisposable + +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Scripts.Components.Dashboards + +@inject ProcessorHubClient ProcessorHub + +Script Manager + +
+
+ + +
+
+ + + +
+
+
+
+
+ +@code { + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + await ProcessorHub.StartAsync(); + } + + public async ValueTask DisposeAsync() + { + await ProcessorHub.StopAsync(); + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Pages/User.razor b/RobotNet.WebApp/Pages/User.razor new file mode 100644 index 0000000..32ac08e --- /dev/null +++ b/RobotNet.WebApp/Pages/User.razor @@ -0,0 +1,25 @@ +@page "/user" +@attribute [Authorize] + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication + +@inject NavigationManager Navigation + + + +
+

User @context.User.Identity?.Name

+
+ Logout +
+
+
+
+ +@code { + + public void BeginLogOut() + { + Navigation.NavigateToLogout("authentication/logout"); + } +} diff --git a/RobotNet.WebApp/Program.cs b/RobotNet.WebApp/Program.cs new file mode 100644 index 0000000..0fc3b63 --- /dev/null +++ b/RobotNet.WebApp/Program.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Authentication; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using MudBlazor.Services; +using RobotNet.WebApp; +using RobotNet.WebApp.Clients; +using RobotNet.WebApp.Helpers; +using System.Globalization; +using System.Security.Claims; + +DefaultPersistentStorageConfigurationExtensions.FixStaticConstructorException(); +CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddOidcAuthentication(options => +{ + builder.Configuration.Bind("Local", options.ProviderOptions); + options.UserOptions.RoleClaim = ClaimTypes.Role; +}); + +var scriptManagerAddress = builder.Configuration["ScriptManager:BaseAddress"] ?? throw new InvalidOperationException("ScriptManager -> BaseAddress not found."); +var robotManagerAddress = builder.Configuration["RobotManager:BaseAddress"] ?? throw new InvalidOperationException("RobotManager -> BaseAddress not found."); +var mapManagerAddress = builder.Configuration["MapManager:BaseAddress"] ?? throw new InvalidOperationException("MapManager -> BaseAddress not found."); + +builder.Services.AddAuthorizationHttpClient("ScriptManagerAPI", scriptManagerAddress); +builder.Services.AddTransient(sp => new ConsoleHubClient(sp.GetRequiredService(), new Uri($"{scriptManagerAddress}/hubs/console"))); +builder.Services.AddTransient(sp => new ProcessorHubClient(sp.GetRequiredService(), new Uri($"{scriptManagerAddress}/hubs/processor"))); +builder.Services.AddTransient(sp => new ScriptTaskHubClient(sp.GetRequiredService(), new Uri($"{scriptManagerAddress}/hubs/scripttask"))); +builder.Services.AddTransient(sp => new DashboardHubClient(sp.GetRequiredService(), new Uri($"{scriptManagerAddress}/hubs/dashboard"))); +builder.Services.AddTransient(sp => new HMIHubClient(sp.GetRequiredService(), new Uri($"{scriptManagerAddress}/hubs/hmi"))); + +builder.Services.AddAuthorizationHttpClient("RobotManagerAPI", robotManagerAddress); +builder.Services.AddTransient(sp => new RobotHubClient(sp.GetRequiredService(), new Uri($"{robotManagerAddress}/hubs/robot/online"))); +builder.Services.AddTransient(sp => new TrafficHubClient(sp.GetRequiredService(), new Uri($"{robotManagerAddress}/hubs/traffic"))); + +builder.Services.AddAuthorizationHttpClient("MapManagerAPI", mapManagerAddress); + +builder.Services.AddMudServices(config => +{ + config.SnackbarConfiguration.VisibleStateDuration = 2000; + config.SnackbarConfiguration.HideTransitionDuration = 500; + config.SnackbarConfiguration.ShowTransitionDuration = 500; +}); + +await builder.Build().RunAsync(); diff --git a/RobotNet.WebApp/Properties/launchSettings.json b/RobotNet.WebApp/Properties/launchSettings.json new file mode 100644 index 0000000..14ef459 --- /dev/null +++ b/RobotNet.WebApp/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "sehc/da3/line1", + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7035", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RobotNet.WebApp/RobotNet.WebApp.csproj b/RobotNet.WebApp/RobotNet.WebApp.csproj new file mode 100644 index 0000000..a5f9da9 --- /dev/null +++ b/RobotNet.WebApp/RobotNet.WebApp.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + enable + enable + service-worker-assets.js + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor new file mode 100644 index 0000000..0bcb3e9 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor @@ -0,0 +1,50 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + +@code { + [Parameter, EditorRequired] + public EdgeModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + private ElementReference Ref; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await UpdatePathData(); + } + + private async Task UpdatePathData() + { + var data = $"M {Model.X1} {Model.Y1}"; + + if (Model.TrajectoryDegree == TrajectoryDegree.One) + { + data = $"{data} L {(Model.X1 + Model.X2) / 2} {(Model.Y1 + Model.Y2) / 2} L {Model.X2} {Model.Y2}"; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Two) + { + data = $"{data} Q {Model.ControlPoint1X} {Model.ControlPoint1Y} {Model.X2} {Model.Y2}"; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Three) + { + data = $"{data} C {Model.ControlPoint1X} {Model.ControlPoint1Y}, {Model.ControlPoint2X} {Model.ControlPoint2Y}, {Model.X2} {Model.Y2}"; + } + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "d", data); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.cs new file mode 100644 index 0000000..dee55f8 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.cs @@ -0,0 +1,38 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.WebApp.Robots.Components.Monitoring.Element; + +public class EdgeModel(EdgeDto data, NodeModel startNode, NodeModel endNode) : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public double X1 => StartNode.X; + public double Y1 => StartNode.Y; + public double X2 => EndNode.X; + public double Y2 => EndNode.Y; + public DirectionAllowed DirectionAllowed => Data.DirectionAllowed; + public TrajectoryDegree TrajectoryDegree => Data.TrajectoryDegree; + public double ControlPoint1X => Data.ControlPoint1X; + public double ControlPoint1Y => Data.ControlPoint1Y; + public double ControlPoint2X => Data.ControlPoint2X; + public double ControlPoint2Y => Data.ControlPoint2Y; + public double MaxSpeed => Data.MaxSpeed; + public double MaxHeight => Data.MaxHeight; + public double MinHeight => Data.MinHeight; + public bool RotationAllowed => Data.RotationAllowed; + public double MaxRotationSpeed => Data.MaxRotationSpeed; + public string Actions => Data.Actions; + public double AllowedDeviationXy => Data.AllowedDeviationXy; + public double AllowedDeviationTheta => Data.AllowedDeviationTheta; + public NodeModel StartNode { get; private set; } = startNode; + public NodeModel EndNode { get; private set; } = endNode; + + + private readonly EdgeDto Data = data; + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.css new file mode 100644 index 0000000..6ac0298 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Edge.razor.css @@ -0,0 +1,24 @@ +path { + cursor: default; + stroke: #22B3FF; + stroke-width: var(--edge-stroke-width); + fill: none; +} + + path.setting:hover { + stroke: #1E9DDF; + cursor: pointer; + } + + path.setting:active { + stroke: #227CFF; + } + + path.setting.active:hover { + stroke: #227CFF; + } + + path.active { + stroke: #227CFF; + } + diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor new file mode 100644 index 0000000..48da35a --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor @@ -0,0 +1,69 @@ +@implements IDisposable + +@inject IJSRuntime JSRuntime + + + + +@code { + [Parameter, EditorRequired] + public EdgeModel Model { get; set; } = null!; + + private ElementReference Ref; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await UpdatePathData(); + await UpdateMaker(); + } + + private async Task UpdatePathData() + { + string data = string.Empty; + double sx = 0, sy = 0, mx = 0, my = 0, ex = 0, ey = 0; + double sTime = 0.49, eTime = 0.5; + + + if (Model.TrajectoryDegree == TrajectoryDegree.One) + { + sx = Model.X1; + sy = Model.Y1; + ex = Model.X2; + ey = Model.Y2; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Two) + { + sx = (Model.X1 + Model.ControlPoint1X) / 2; + sy = (Model.Y1 + Model.ControlPoint1Y) / 2; + ex = (Model.X2 + Model.ControlPoint1X) / 2; + ey = (Model.Y2 + Model.ControlPoint1Y) / 2; + } + else if (Model.TrajectoryDegree == TrajectoryDegree.Three) + { + sx = Math.Pow(1 - sTime, 3) * Model.X1 + 3 * Math.Pow(1 - sTime, 2) * sTime * Model.ControlPoint1X + 3 * Math.Pow(sTime, 2) * (1 - sTime) * Model.ControlPoint2X + Math.Pow(sTime, 3) * Model.X2; + sy = Math.Pow(1 - sTime, 3) * Model.Y1 + 3 * Math.Pow(1 - sTime, 2) * sTime * Model.ControlPoint1Y + 3 * Math.Pow(sTime, 2) * (1 - sTime) * Model.ControlPoint2Y + Math.Pow(sTime, 3) * Model.Y2; + + ex = Math.Pow(1 - eTime, 3) * Model.X1 + 3 * Math.Pow(1 - eTime, 2) * eTime * Model.ControlPoint1X + 3 * Math.Pow(eTime, 2) * (1 - eTime) * Model.ControlPoint2X + Math.Pow(eTime, 3) * Model.X2; + ey = Math.Pow(1 - eTime, 3) * Model.Y1 + 3 * Math.Pow(1 - eTime, 2) * eTime * Model.ControlPoint1Y + 3 * Math.Pow(eTime, 2) * (1 - eTime) * Model.ControlPoint2Y + Math.Pow(eTime, 3) * Model.Y2; + } + + mx = (sx + ex) / 2; + my = (sy + ey) / 2; + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", $"{sx},{sy} {mx},{my} {ex},{ey}"); + } + + private async Task UpdateMaker() + { + if (Model == null) return; + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "marker-mid", MapSvgDefs.GetMakerMid(Model.DirectionAllowed)); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor.css new file mode 100644 index 0000000..08b21eb --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/EdgeDirection.razor.css @@ -0,0 +1,5 @@ +polyline { + stroke-width: var(--edge-direction-stroke-width); + stroke: none; + fill: none; +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor new file mode 100644 index 0000000..5d4ed68 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor @@ -0,0 +1,95 @@ +@implements IDisposable + +@inject IHttpClientFactory HttpFactory +@inject IJSRuntime JSRuntime + + + + @Model?.Name + + + +@code { + [Parameter] + public bool Show { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter, EditorRequired] + public ElementModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + private ElementReference ImageRef; + private ElementReference textRef; + private ElementModelDto ElementModel = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var model = await Http.GetFromJsonAsync>($"api/ElementModels/{Model.ModelId}"); + if (model is not null && model.Data is not null) + { + await JSRuntime.InvokeVoidAsync("SetImageAttribute", ImageRef, model.Data.Width, model.Data.Height, -model.Data.Width / 2, -model.Data.Height / 2, $"{Http.BaseAddress}api/images/elementModel/{model.Data.Id}?IsOpen={Model.IsOpen}"); + ElementModel = model.Data; + } + if (Model is not null) + { + Model.UpdateChanged += Model_UpdateChanged; + await UpdatePosition(Model.X, Model.Y, Model.Theta); + } + } + + private async Task Model_UpdateChanged() + { + await SetElementImage(Model.IsOpen); + await UpdatePosition(Model.OffsetX, Model.OffsetY); + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateRobot = false; + if (parameters.TryGetValue(nameof(Model), out ElementModel? model) && ((Model?.Id ?? Guid.Empty) != (model?.Id ?? Guid.Empty))) + { + if (Model != null) Model.UpdateChanged -= Model_UpdateChanged; + if (model != null) + { + updateRobot = true; + model.UpdateChanged -= Model_UpdateChanged; + } + } + await base.SetParametersAsync(parameters); + if (updateRobot && model != null) + { + await SetElementImage(model.IsOpen); + await UpdatePosition(model.X, model.Y, model.Theta); + } + } + + private async Task SetElementImage(bool isOpen) + { + if (ElementModel.Id != Guid.Empty) + { + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + await JSRuntime.InvokeVoidAsync("SetImageAttribute", ImageRef, ElementModel.Width, ElementModel.Height, (-ElementModel.Width / 2), (-ElementModel.Height / 2), $"{Http.BaseAddress}api/images/elementModel/{ElementModel.Id}?IsOpen={isOpen}"); + } + } + + public async Task UpdatePosition(double x, double y, double theta) + => await JSRuntime.InvokeVoidAsync("SetRobotPosition", ImageRef, textRef, x + Model.OffsetX, y + Model.OffsetY, theta, -ElementModel.Width / 2, -ElementModel.Height / 2); + + public async Task UpdatePosition(double offsetX, double offsetY) + => await JSRuntime.InvokeVoidAsync("SetRobotPosition", ImageRef, textRef, Model.X + offsetX, Model.Y + offsetY, Model.Theta, -ElementModel.Width / 2, -ElementModel.Height / 2); + + + public void Dispose() + { + if (Model != null) Model.UpdateChanged -= Model_UpdateChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.cs new file mode 100644 index 0000000..d41177d --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.cs @@ -0,0 +1,38 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.WebApp.Robots.Components.Monitoring.Element; + +public class ElementModel(ElementDto element, NodeModel node) : IDisposable +{ + public Guid Id => Element.Id; + public Guid ModelId => Element.ModelId; + public Guid NodeId => Element.NodeId; + public Guid MapId => Element.MapId; + public string Name => Element.Name; + public double X => Node.X; + public double Y => Node.Y; + public double Theta => Node.Theta; + public double OffsetX => Element.OffsetX; + public double OffsetY => Element.OffsetY; + public bool IsOpen => Element.IsOpen; + public string Content => Element.Content; + + public ElementDto Element { get; private set; } = element; + public NodeModel Node { get; private set; } = node; + + public event Func? UpdateChanged; + + public void Update(ElementDto element) + { + Element.IsOpen = element.IsOpen; + Element.OffsetX = element.OffsetX; + Element.OffsetY = element.OffsetY; + Element.Content = element.Content; + UpdateChanged?.Invoke(); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.css new file mode 100644 index 0000000..9fe7051 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Element.razor.css @@ -0,0 +1,21 @@ +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + font-size: 0.01em; + user-select: none; + font-weight: normal; +} + +.element { + transform-origin: center; + transform-box: fill-box; +} + + .element:hover { + cursor: pointer; + transform: scale(1.2); + filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5)); + transition: all 0.2s ease; + } \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor new file mode 100644 index 0000000..93c5db8 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor @@ -0,0 +1,47 @@ +@using Microsoft.JSInterop + +@implements IDisposable +@inject IJSRuntime JSRuntime + + + +@Model?.Name + +@code { + [Parameter, EditorRequired] + public NodeModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + private ElementReference circleRef; + private ElementReference textRef; + private ElementReference circleErrorRef; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + await UpdatePosition(Model.X, Model.Y); + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", circleErrorRef, "visibility", "hidden"); + } + + private async Task UpdatePosition(double x, double y) + { + await JSRuntime.InvokeVoidAsync("SetNodePosition", circleRef, textRef, x, y); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.cs new file mode 100644 index 0000000..a4ad9ff --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.cs @@ -0,0 +1,21 @@ +using RobotNet.MapShares.Dtos; + +namespace RobotNet.WebApp.Robots.Components.Monitoring.Element; + +public class NodeModel(NodeDto Data) : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public string Name => Data.Name; + public double X => Data.X; + public double Y => Data.Y; + public double Theta => Data.Theta; + public string Actions => Data.Actions; + public double AllowedDeviationXy => Data.AllowedDeviationXy; + public double AllowedDeviationTheta => Data.AllowedDeviationTheta; + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.css new file mode 100644 index 0000000..b088c2c --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Node.razor.css @@ -0,0 +1,41 @@ +circle { + cursor: default; + fill: #FFBD33; + stroke: #FF5733; + stroke-width: 0; + r: var(--node-r); +} + + circle.setting:hover { + stroke-width: 0.02px; + cursor: pointer; + } + + circle.setting:active { + stroke-width: 0.02px; + stroke: green; + } + + circle.active { + stroke-width: 0.02px; + stroke: green; + } + + circle:hover + text.setting { + transition-delay: 0.1s; + font-weight: bold; + } + + circle + text { + transition-delay: 0.3s; + font-size: 0.01em; + font-weight: normal; + } + +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + user-select: none; +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor new file mode 100644 index 0000000..e43c16f --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor @@ -0,0 +1,111 @@ +@implements IDisposable + +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpFactory + + + +@Model?.RobotName + +@code { + [Parameter, EditorRequired] + public RobotVisualizationModel Model { get; set; } = null!; + + [Parameter] + public bool ShowName { get; set; } + + private ElementReference ElementRef; + private ElementReference RobotRef; + private ElementReference textRef; + private RobotModelDto RobotModel = new(); + + private bool isCarrying = false; + private ElementModelDto ElementModel = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var model = await Http.GetFromJsonAsync>($"api/Robots/Model/{Model.RobotId}"); + if (model is not null && model.Data is not null) + { + await JSRuntime.InvokeVoidAsync("SetImageAttribute", RobotRef, model.Data.Length, model.Data.Width, model.Data.OriginX, model.Data.OriginY, $"{Http.BaseAddress}api/RobotModels/image/{model.Data.Id}"); + RobotModel = model.Data; + } + if (Model is not null) + { + Model.PositionChanged += Model_PositionChanged; + Model.LoadsChanged += Model_LoadChanged; + await UpdatePosition(Model.X, Model.Y, Model.Theta); + if(Model.Loads.Length > 0 && Guid.TryParse(Model.Loads[0].LoadId, out Guid loadId)) + { + await Model_LoadChanged(loadId); + await UpdateLoadPosition(Model.X, Model.Y, Model.Theta); + } + } + } + + public override async Task SetParametersAsync(ParameterView parameters) + { + bool updateRobot = false; + if (parameters.TryGetValue(nameof(Model), out RobotVisualizationModel? model) && ((Model?.RobotId ?? string.Empty) != (model?.RobotId ?? string.Empty))) + { + if (Model != null) + { + Model.PositionChanged -= Model_PositionChanged; + Model.LoadsChanged -= Model_LoadChanged; + } + + if (model != null) + { + updateRobot = true; + model.PositionChanged += Model_PositionChanged; + model.LoadsChanged += Model_LoadChanged; + } + } + await base.SetParametersAsync(parameters); + if (updateRobot && model != null) + { + await UpdatePosition(model.X, model.Y, model.Theta); + if (model.Loads.Length > 0) await UpdateLoadPosition(model.X, model.Y, model.Theta); + } + } + + private async Task Model_PositionChanged() + { + await UpdatePosition(Model.X, Model.Y, Model.Theta); + await UpdateLoadPosition(Model.X, Model.Y, Model.Theta); + } + + public async Task UpdatePosition(double x, double y, double theta) + => await JSRuntime.InvokeVoidAsync("SetRobotPosition", RobotRef, textRef, x, y, theta, RobotModel.OriginX, RobotModel.OriginY); + + private async Task UpdateLoadPosition(double x, double y, double theta) + => await JSRuntime.InvokeVoidAsync("SetRobotPosition", ElementRef, null, x, y, theta, -ElementModel.Width / 2, -ElementModel.Height / 2); + + private async Task Model_LoadChanged(Guid elementModelId) + { + if (elementModelId != Guid.Empty) + { + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var model = await Http.GetFromJsonAsync>($"api/ElementModels/{elementModelId}"); + if (model is not null && model.Data is not null) + { + isCarrying = true;; + await JSRuntime.InvokeVoidAsync("SetImageAttribute", ElementRef, model.Data.Width, model.Data.Height, -model.Data.Width / 2, -model.Data.Height / 2, $"{Http.BaseAddress}api/images/elementModel/{model.Data.Id}?IsOpen=true"); + await UpdateLoadPosition(Model.X, Model.Y, Model.Theta); + ElementModel = model.Data; + } + } + else isCarrying = false; + StateHasChanged(); + } + + public void Dispose() + { + if (Model != null) Model.PositionChanged -= Model_PositionChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.cs new file mode 100644 index 0000000..a0362eb --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.cs @@ -0,0 +1,43 @@ +using RobotNet.RobotShares.VDA5050.State; + +namespace RobotNet.WebApp.Robots.Components.Monitoring.Element; + +#nullable disable + +public class RobotVisualizationModel : IDisposable +{ + public string RobotId { get; set; } + public string RobotName { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Theta { get; set; } + public Load[] Loads { get; set; } = []; + public event Func PositionChanged; + public event Func LoadsChanged; + public void Update(double x, double y, double theta, Load[] loads) + { + if (X != x || Y != y || Theta != theta) + { + X = x; + Y = y; + Theta = theta; + PositionChanged?.Invoke(); + } + + if (loads.Length > 0 && (Loads.Length == 0 || Loads[0].LoadId != loads[0].LoadId)) + { + if (Guid.TryParse(loads[0].LoadId, out Guid elementModelId)) LoadsChanged?.Invoke(elementModelId); + else LoadsChanged?.Invoke(Guid.Empty); + Loads = loads; + } + else if (Loads.Length != 0 && loads.Length == 0) + { + LoadsChanged?.Invoke(Guid.Empty); + Loads = loads; + } + } + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.css new file mode 100644 index 0000000..7b881b0 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Robot.razor.css @@ -0,0 +1,13 @@ +text { + dominant-baseline: middle; + text-anchor: middle; + fill: red; + transform: scale(1, -1); + font-size: 0.01em; + user-select: none; + font-weight: normal; +} +/* +image { + transform: scale(1, -1); +}*/ \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor new file mode 100644 index 0000000..873da5d --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor @@ -0,0 +1,40 @@ +@inject IJSRuntime JSRuntime +@implements IDisposable + + + +@code { + [Parameter, EditorRequired] + public ZoneModel Model { get; set; } = null!; + + [Parameter] + public EventCallback DoubleClick { get; set; } + + [Parameter] + public bool IsShow { get; set; } + + [CascadingParameter] + protected bool MapIsActive { get; set; } + + private ElementReference Ref; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await UpdateData(); + } + + private async Task UpdateData() + { + string data = $"{Model.X1},{Model.Y1} {Model.X2},{Model.Y2} {Model.X3},{Model.Y3} {Model.X4},{Model.Y4}"; + + await JSRuntime.InvokeVoidAsync("ElementSetAttribute", Ref, "points", data); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.cs new file mode 100644 index 0000000..79bbb21 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.cs @@ -0,0 +1,35 @@ +using RobotNet.MapShares.Dtos; +using RobotNet.MapShares.Enums; + +namespace RobotNet.WebApp.Robots.Components.Monitoring.Element; + +public class ZoneModel(ZoneDto Data) : IDisposable +{ + public Guid Id => Data.Id; + public Guid MapId => Data.MapId; + public ZoneType Type => Data.Type; + + public double X1 => Data.X1; + public double Y1 => Data.Y1; + public double X2 => Data.X2; + public double Y2 => Data.Y2; + public double X3 => Data.X3; + public double Y3 => Data.Y3; + public double X4 => Data.X4; + public double Y4 => Data.Y4; + + public string Fill => Data.Type switch + { + ZoneType.Confined => "#c29ffa", // Vùng hoạt động hạn chế + ZoneType.Forbidden => "#ea868f", // Vùng cấm di chuyển + ZoneType.Operating => "#79dfc1", // Vùng hoạt động bình thường + ZoneType.OperatingHazard => "#ffda6a", // Vùng hoạt động nguy hiểm + ZoneType.Restricted => "#fd9843", // Vùng hoạt động giới hạn + ZoneType.LoadTransfer => "#e685b5", // Vùng chuyển hàng + _ => "none", + }; + public void Dispose() + { + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.css new file mode 100644 index 0000000..4c27723 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/Element/Zone.razor.css @@ -0,0 +1,29 @@ +polygon { + stroke: none; + fill-opacity: 0.5; + stroke-linejoin: round; + stroke-linecap: butt; + position: relative; + z-index: 1; +} + + polygon.setting:hover { + stroke-width: 0.02px; + stroke: red; + cursor: pointer; + } + + polygon.setting:active { + fill-opacity: 0.7; + stroke-width: 0.02px; + stroke: green; + cursor: pointer; + } + + polygon.setting.active { + fill-opacity: 0.7; + stroke-width: 0.02px; + stroke: green; + cursor: pointer; + z-index: 2; + } diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor new file mode 100644 index 0000000..7681195 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor @@ -0,0 +1,37 @@ + + @for (int x = startX; x <= endX; x += 1) + { + + } + @for (int y = startY; y <= endY; y += 1) + { + + } + + +@code { + [Parameter, EditorRequired] + public bool Show { get; set; } + + private double originX; + private double originY; + private int startX; + private int startY; + private int endX; + private int endY; + private double width; + private double height; + + public void Resize(double mapOriginX, double mapOriginY, double mapheight, double mapwidth) + { + width = mapwidth; + height = mapheight; + originX = mapOriginX; + originY = -height - mapOriginY; + startX = (int)Math.Ceiling(originX); + startY = (int)Math.Ceiling(originY); + endX = (int)Math.Floor(width + mapOriginX); + endY = (int)Math.Floor(-mapOriginY); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor.css new file mode 100644 index 0000000..ca81f84 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapGrid.razor.css @@ -0,0 +1,6 @@ +line { + stroke: gray; + stroke-width: 0.04px; + stroke-opacity: 1; + stroke-dasharray: 0.05 0.1; +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor new file mode 100644 index 0000000..c4142f2 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor @@ -0,0 +1,20 @@ +
+
+ @X +
+
+ @Y +
+
+ +@code { + private double X; + private double Y; + + public void Update(double x, double y) + { + X = Math.Round(x, 3); + Y = Math.Round(y, 3); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor.css new file mode 100644 index 0000000..d40e5cf --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapMousePosition.razor.css @@ -0,0 +1,14 @@ +div { + width:fit-content; + height: fit-content; +} +.map-editor-info { + color: #00cc66; + font-size: 15px; + position: absolute; + top: 5px; + left: 5px; + font-weight: bold; + background-color:white; + border-radius:4px +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor new file mode 100644 index 0000000..8539723 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor @@ -0,0 +1,30 @@ +@implements IDisposable + +@using RobotNet.WebApp.Robots.Components.Monitoring.Element + +@foreach (var robot in Models) +{ + +} + + +@code { + [Parameter, EditorRequired] + public MapRobotModel Models { get; set; } = null!; + + [Parameter] + public bool ShowName { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + Models.Changed += StateHasChanged; + } + + public void Dispose() + { + Models.Changed -= StateHasChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor.cs new file mode 100644 index 0000000..970f1c9 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapRobot.razor.cs @@ -0,0 +1,49 @@ +using RobotNet.WebApp.Robots.Components.Monitoring.Element; +using System.Collections; + +namespace RobotNet.WebApp.Robots.Components.Monitoring; + +public class MapRobotModel : IEnumerable +{ + private readonly Dictionary dicRobots = []; + public IEnumerator GetEnumerator() => dicRobots.Values.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public event Action? Changed; + + public void Add(RobotVisualizationModel robot) + { + if (dicRobots.ContainsKey(robot.RobotId)) return; + + dicRobots.Add(robot.RobotId, robot); + Changed?.Invoke(); + } + + public bool ContainsKey(string key) => dicRobots.ContainsKey(key); + + public void ReplaceAll(IEnumerable robots) + { + foreach (var robot in dicRobots.Values) + { + robot.Dispose(); + } + dicRobots.Clear(); + + foreach (var robot in robots) + { + robot.Update(robot.X, robot.Y, robot.Theta, robot.Loads); + dicRobots.Add(robot.RobotId, robot); + } + Changed?.Invoke(); + } + + public void Remove(RobotVisualizationModel robot) + { + if (!dicRobots.ContainsKey(robot.RobotId)) return; + robot.Dispose(); + dicRobots.Remove(robot.RobotId); + Changed?.Invoke(); + } + + public bool TryGetValue(string id, out RobotVisualizationModel? value) => dicRobots.TryGetValue(id, out value); +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor new file mode 100644 index 0000000..2ff5640 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + +@code { + public static string GetMakerMid(DirectionAllowed direction) + { + return direction switch + { + DirectionAllowed.Both => $"url(#edge-two-way)", + DirectionAllowed.Forward => $"url(#edge-forward)", + DirectionAllowed.Backward => $"url(#edge-backward)", + DirectionAllowed.None => $"url(#edge-none)", + _ => "", + }; + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor.css new file mode 100644 index 0000000..1a2be22 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MapSvgDefs.razor.css @@ -0,0 +1,5 @@ +polyline { + stroke: #DD22FF; + stroke-width: 0.05px; + fill: none; +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MonitorInfomation.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorInfomation.razor new file mode 100644 index 0000000..9806899 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorInfomation.razor @@ -0,0 +1,125 @@ +@using RobotNet.RobotShares.VDA5050.State + + + +@code { + private RobotInfomationDto RobotStateSelected = new(); + private bool IsExpand = true; + + public void ExpandedClick(bool value) + { + IsExpand = value; + StateHasChanged(); + } + + public void UpdateState(RobotInfomationDto data) + { + RobotStateSelected.Battery.BatteryVoltage = data.Battery.BatteryVoltage; + RobotStateSelected.Battery.BatteryHealth = data.Battery.BatteryHealth; + RobotStateSelected.Battery.BatteryCharge = data.Battery.BatteryCharge; + RobotStateSelected.Battery.Charging = data.Battery.Charging; + + RobotStateSelected.AgvPosition.LocalizationScore = data.AgvPosition.LocalizationScore; + RobotStateSelected.AgvPosition.PositionInitialized = data.AgvPosition.PositionInitialized; + RobotStateSelected.AgvPosition.DeviationRange = data.AgvPosition.DeviationRange; + + RobotStateSelected.AgvPosition.X = data.AgvPosition.X; + RobotStateSelected.AgvPosition.Y = data.AgvPosition.Y; + RobotStateSelected.AgvPosition.Theta = data.AgvPosition.Theta; + RobotStateSelected.AgvVelocity.Vx = data.AgvVelocity.Vx; + RobotStateSelected.AgvVelocity.Vy = data.AgvVelocity.Vy; + RobotStateSelected.AgvVelocity.Omega = data.AgvVelocity.Omega; + + RobotStateSelected.Errors = [.. data.Errors]; + RobotStateSelected.Infomations = [.. data.Infomations]; + StateHasChanged(); + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor new file mode 100644 index 0000000..d55e1b5 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor @@ -0,0 +1,501 @@ +@using RobotNet.WebApp.Robots.Components.Monitoring.Element +@using System.Text.Json + +@inject IJSRuntime JSRuntime +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject IHttpClientFactory HttpFactory + +
+
+ + + + + + + @foreach (var zone in zones) + { + + } + @foreach (var edge in edges) + { + + } + @foreach (var edge in edges) + { + + } + @foreach (var node in nodes) + { + + } + @foreach (var element in elements) + { + + } + + + + + +
+ +
+ + + + + Update Element @ElementUpdateModel.Name + + + +
+
+ + + + +
+
+
+ @foreach (var property in ElementProperties) + { + @if (property.Type == typeof(int).ToString()) + { + if (int.TryParse(property.DefaultValue, out int value)) + { + + } + } + else if (property.Type == typeof(double).ToString()) + { + if (double.TryParse(property.DefaultValue, out double value)) + { + + } + } + else if (property.Type == typeof(bool).ToString()) + { + if (bool.TryParse(property.DefaultValue, out bool value)) + { + + } + } + else if (property.Type == typeof(string).ToString()) + { + + } + } +
+ Properties +
+
+
+ + Cancel + Update + +
+ +@code { + + [Parameter] + public bool ShowGrid { get; set; } + + [Parameter] + public bool ShowName { get; set; } + + [Parameter] + public bool ShowPath { get; set; } + + [Parameter] + public bool ShowLaser { get; set; } + + [Parameter] + public bool ShowElement { get; set; } + + private MapMousePosition MapInfoRef = null!; + private RobotPath RobotPathRef = null!; + private RobotCurrentPath RobotCurrentPathRef = null!; + private MapGrid MapGridRef = null!; + private RobotLaserScaner RobotLaserScanerRef = null!; + + private DotNetObjectReference DotNetObj = null!; + private ElementReference ViewContainerRef; + private ElementReference ViewMovementRef; + private ElementReference MapContainerRef; + private ElementReference MapImageRef; + + private double ViewContainerRectX; + private double ViewContainerRectY; + private double ViewContainerRectWidth; + private double ViewContainerRectHeight; + private double ViewContainerRectTop; + private double ViewContainerRectRight; + private double ViewContainerRectBottom; + private double ViewContainerRectLeft; + + private double CursorX; + private double CursorY; + private double ClientOriginX; + private double ClientOriginY; + + private double Resolution = 1; + private double OriginX; + private double OriginY; + private double ImageOriginY; + private double ImageWidth; + private double ImageHeight; + private double Scale = 1; + private double Left; + private double Top; + private double FitScale = 1; + private double OldOriginY; + + private List edges = new(); + private List nodes = new(); + private List zones = new(); + private List elements = new(); + private MapRobotModel robots = new(); + + private bool IsFocusRobot = false; + private string RobotSelectedId = ""; + + private bool updateElementVisble; + private ElementUpdateModel ElementUpdateModel = new(); + private List ElementProperties = []; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + DotNetObj = DotNetObjectReference.Create(this); + + await JSRuntime.InvokeVoidAsync("DOMCssLoaded"); + + await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await JSRuntime.InvokeVoidAsync("ResizeObserverRegister", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await JSRuntime.InvokeVoidAsync("AddEventListener", DotNetObj, ViewContainerRef, "click", nameof(ViewContainerClick)); + await JSRuntime.InvokeVoidAsync("AddMouseMoveEventListener", DotNetObj, ViewContainerRef, nameof(MouseMoveOnMapContainer)); + + await JSRuntime.InvokeVoidAsync("AddMouseWheelEventListener", DotNetObj, MapContainerRef, nameof(MouseWheelOnMapContainer)); + await JSRuntime.InvokeVoidAsync("AddTouchMoveEventListener", DotNetObj, ViewContainerRef, nameof(TouchMoveOnMapContainer)); + } + + public async Task LoadMap(MapDataDto? mapData) + { + if (mapData == null) + { + nodes.Clear(); + edges.Clear(); + zones.Clear(); + elements.Clear(); + robots.ReplaceAll([]); + RobotCurrentPathRef.UpdatePath([]); + RobotPathRef.UpdatePath([]); + Resolution = 1.0; + OriginY = 0; + OriginX = 0; + OldOriginY = 0; + ImageOriginY = 0; + ImageHeight = 0; + ImageWidth = 0; + MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth); + await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, 1.0); + await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, ""); + } + else + { + nodes = [.. mapData.Nodes.Select(node => new NodeModel(node))]; + edges = [.. mapData.Edges.Select(edge => new EdgeModel(edge, nodes.First(n => n.Id == edge.StartNodeId), nodes.First(n => n.Id == edge.EndNodeId)))]; + zones = [.. mapData.Zones.Select(zone => new ZoneModel(zone))]; + elements = [.. mapData.Elements.Select(element => new ElementModel(element, nodes.First(n => n.Id == element.NodeId)))]; + Resolution = mapData.Resolution; + OldOriginY = mapData.OriginY; + OriginY = -mapData.ImageHeight * Resolution - mapData.OriginY; + OriginX = mapData.OriginX; + ImageOriginY = mapData.OriginY; + ImageHeight = mapData.ImageHeight * Resolution; + ImageWidth = mapData.ImageWidth * Resolution; + MapGridRef.Resize(OriginX, OriginY, ImageHeight, ImageWidth); + + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + + await JSRuntime.InvokeVoidAsync("SetMapSvgConfig", MapContainerRef, ImageWidth, ImageHeight, OriginX, mapData.OriginY); + await JSRuntime.InvokeVoidAsync("SetImageAttribute", MapImageRef, ImageWidth, ImageHeight, OriginX, OriginY, $"{Http.BaseAddress}api/images/map/{mapData.Id}"); + await JSRuntime.InvokeVoidAsync("UpdateViewContainerRect", DotNetObj, ViewContainerRef, nameof(ViewContainerResize)); + await ScaleFitContentAsync(); + } + StateHasChanged(); + } + + public async Task ScaleFitContentAsync() + { + Scale = FitScale; + await SetViewMovement((ViewContainerRectWidth - ImageWidth * Scale) / 2, (ViewContainerRectHeight - ImageHeight * Scale) / 2); + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + } + + private async Task SetViewMovement(double left, double top) + { + Top = top; + Left = left; + + ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale; + ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale; + await JSRuntime.InvokeVoidAsync("SetMapMovement", ViewMovementRef, Top, Left); + } + + public async Task ScaleZoom(double deltaY) + { + if (deltaY > 0) + { + if (Scale >= FitScale * 20) return; + } + else + { + if (Scale <= FitScale / 2) return; + } + double oldScale = Scale; + Scale += deltaY; + + double centerXBefore = ((ViewContainerRectLeft + ViewContainerRectWidth / 2) - ClientOriginX) / oldScale - OriginX; + double centerYBefore = (ClientOriginY - (ViewContainerRectHeight / 2 + ViewContainerRectTop)) / oldScale - OldOriginY; + + await SetViewMovement(Left - centerXBefore * deltaY, Top - (ImageHeight - centerYBefore) * deltaY); + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + } + + [JSInvokable] + public void ViewContainerResize(double x, double y, double width, double height, double top, double right, double bottom, double left) + { + ViewContainerRectX = x; + ViewContainerRectY = y; + ViewContainerRectWidth = width; + ViewContainerRectHeight = height; + ViewContainerRectTop = top; + ViewContainerRectRight = right; + ViewContainerRectBottom = bottom; + ViewContainerRectLeft = left; + + ClientOriginX = ViewContainerRectLeft + Left - OriginX * Scale; + ClientOriginY = ViewContainerRectTop + Top - OriginY * Scale; + + FitScale = Math.Min(ViewContainerRectWidth / ImageWidth, ViewContainerRectHeight / ImageHeight); + } + + [JSInvokable] + public async Task TouchMoveOnMapContainer(double clientX, double clientY, int touchCount, double movementX, double movementY) + { + CursorX = (clientX - ClientOriginX) / Scale; + CursorY = (ClientOriginY - clientY) / Scale; + MapInfoRef.Update(CursorX, CursorY); + + if (touchCount == 1) await SetViewMovement(Left + movementX, Top + movementY); + } + + [JSInvokable] + public async Task MouseWheelOnMapContainer(double deltaY, double offsetX, double offsetY) + { + double scaleChange; + if (deltaY > 0) + { + if (Scale <= FitScale / 2) return; + scaleChange = Scale > FitScale ? -(Scale / FitScale) : -0.1; + } + else + { + if (Scale >= FitScale * 100) return; + scaleChange = Scale < FitScale ? 0.5 : (Scale / FitScale); + } + + double oldScale = Scale; + + Scale += scaleChange; + await JSRuntime.InvokeVoidAsync("SetMapSvgRect", MapContainerRef, ImageWidth * Scale, ImageHeight * Scale); + + double mouseX = CursorX - OriginX; + double mouseY = CursorY - OldOriginY; + await SetViewMovement(Left - mouseX * scaleChange, Top - (ImageHeight - mouseY) * scaleChange); + } + + [JSInvokable] + public async Task ViewContainerClick() => await ViewContainerRef.FocusAsync(); + + [JSInvokable] + public async Task MouseMoveOnMapContainer(double clientX, double clientY, long buttons, bool ctrlKey, double movementX, double movementY) + { + CursorX = (clientX - ClientOriginX) / Scale; + CursorY = (ClientOriginY - clientY) / Scale; + MapInfoRef.Update(CursorX, CursorY); + + if (IsFocusRobot) return; + switch (buttons) + { + case 4: + await SetViewMovement(Left + movementX, Top + movementY); + break; + } + } + + public void LoadRobots(IEnumerable robotDtos) + { + if (robotDtos.Any()) + { + var robotModel = robotDtos.Select(robot => new RobotVisualizationModel() + { + RobotId = robot.RobotId, + RobotName = robot.Name, + X = robot.AgvPosition.X, + Y = robot.AgvPosition.Y, + Theta = robot.AgvPosition.Theta, + }).ToArray(); + robots.ReplaceAll(robotModel); + } + else robots.ReplaceAll([]); + StateHasChanged(); + } + + public void RobotSelectedChange(string robotId) + { + if (robotId != RobotSelectedId) RobotSelectedId = robotId; + } + + public void FocusRobot(bool value) => IsFocusRobot = value; + + public async Task FocusRobotAsync() + { + var robot = robots.FirstOrDefault(r => r.RobotId == RobotSelectedId); + if (robot is null) return; + double mapX = (ViewContainerRectWidth - ImageWidth * Scale) / 2 + ((ImageWidth / 2 + OriginX) * Scale) - robot.X * Scale; + double mapY = (ViewContainerRectHeight - ImageHeight * Scale) / 2 + ((ImageHeight / 2 + OriginY) * Scale) + robot.Y * Scale; + await SetViewMovement(mapX, mapY); + } + + public async Task SetRobotPosition(List robotStates) + { + foreach (var robot in robotStates) + { + var robotModel = robots.FirstOrDefault(rm => rm.RobotId == robot.RobotId); + if (robotModel is not null) + { + robotModel.Update(robot.AgvPosition.X, robot.AgvPosition.Y, robot.AgvPosition.Theta, robot.Loads); + if (IsFocusRobot && robot.RobotId == RobotSelectedId) await FocusRobotAsync(); + } + else + { + robots.Add(new() + { + RobotId = robot.RobotId, + RobotName = robot.Name, + X = robot.AgvPosition.X, + Y = robot.AgvPosition.Y, + Theta = robot.AgvPosition.Theta, + Loads = robot.Loads + }); + StateHasChanged(); + } + if (robot.RobotId == RobotSelectedId) + { + RobotPathRef.UpdatePath([.. robot.Navigation.RobotPath]); + RobotCurrentPathRef.UpdatePath([.. robot.Navigation.RobotBasePath]); + RobotLaserScanerRef.SetData(robot.Navigation.LaserScaner); + } + } + } + + public void ElementStateUpdated(IEnumerable elementsState) + { + foreach (var elmenet in elementsState) + { + var elemetModel = elements.FirstOrDefault(e => e.Id == elmenet.Id); + if (elemetModel is not null) + { + elemetModel.Update(elmenet); + } + else + { + var node = nodes.FirstOrDefault(n => n.Id == elmenet.NodeId); + if (node is not null) elements.Add(new ElementModel(elmenet, node)); + } + } + StateHasChanged(); + } + + private void DefaultIntValueChanged(Guid id, int value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultDoubleValueChanged(Guid id, double value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void DefaultBooleanValueChanged(Guid id, bool value) + { + var property = ElementProperties.FirstOrDefault(p => p.Id == id); + if (property is null) return; + property.DefaultValue = value.ToString(); + } + + private void ElementDoubleClick(ElementModel model) + { + ElementUpdateModel.Id = model.Id; + ElementUpdateModel.Name = model.Name; + ElementUpdateModel.IsOpen = model.IsOpen; + ElementUpdateModel.OffsetX = model.OffsetX; + ElementUpdateModel.OffsetY = model.OffsetY; + ElementUpdateModel.Content = model.Content; + + if (model is not null && !string.IsNullOrEmpty(model.Content)) + { + var properties = JsonSerializer.Deserialize>(model.Content, JsonOptionExtends.Read); + if (properties is not null) ElementProperties = [.. properties]; + } + + updateElementVisble = true; + StateHasChanged(); + } + + private async Task UpdateElement() + { + var selectedElement = elements.FirstOrDefault(e => e.Id == ElementUpdateModel.Id); + if (selectedElement is null) return; + + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var result = await (await Http.PutAsJsonAsync($"api/Elements", new ElementUpdateModel() + { + Id = ElementUpdateModel.Id, + Name = ElementUpdateModel.Name, + IsOpen = ElementUpdateModel.IsOpen, + OffsetX = ElementUpdateModel.OffsetX, + OffsetY = ElementUpdateModel.OffsetY, + Content = JsonSerializer.Serialize(ElementProperties, JsonOptionExtends.Write), + })).Content.ReadFromJsonAsync>(); + if (result == null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message, Severity.Error); + else if (result.Data is null) Snackbar.Add("Lỗi dữ liệu", Severity.Error); + else + { + selectedElement.Update(result.Data); + Snackbar.Add("Cập nhật thành công", Severity.Success); + } + + updateElementVisble = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor.css new file mode 100644 index 0000000..49a5cda --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorMap.razor.css @@ -0,0 +1,57 @@ +.monitor-map { + background-color: #808080; + width: 100%; + height: 100%; + cursor: not-allowed; + border-top: solid 2px #808080; + overflow: hidden; + position: relative; +} + + .monitor-map > div { + width: fit-content; + height: fit-content; + position: absolute; + cursor: default; + display: flex; + } + +.monitor-map-image { + user-select: none; + image-rendering: pixelated; + transform: scale(1, -1); +} + +.monitor-map-view { + transform: scale(1, -1); +} + +.paper-property-container { + position: relative; + display: inline-block; + width: 50%; + height: 240px; +} + +.paper-title { + font-size: 12px; + padding: 0 5px; + position: absolute; + background-color: var(--mud-palette-surface); + top: 0px; + left: 15px; +} + +.paper-property { + padding: 10px; + border-radius: var(--mud-default-borderradius); + border: 1px solid var(--mud-palette-lines-inputs); + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + overflow-x: hidden; + overflow-y: auto; + margin: 8px; +} \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor new file mode 100644 index 0000000..5ccdd0a --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor @@ -0,0 +1,96 @@ +
+
+ + + + +
+ + + + + + +
+
+
+ + @foreach(var map in Maps) + { + @map.Name + } + + + @foreach (var robot in Robots) + { + @robot.Name + } + + +
+
+ +@code { + [Parameter] + public MonitorToolBarModel Model { get; set; } = null!; + + [Parameter] + public EventCallback ButtonEventClick { get; set; } + + [Parameter] + public EventCallback ExpandChanged { get; set; } + + [Parameter] + public EventCallback MapChanged { get; set; } + + [Parameter] + public EventCallback RobotChanged { get; set; } + + [Parameter] + public Func? CheckedEventClick { get; set; } + + public List Maps = []; + private MapInfoDto? MapSelected = default!; + + private bool isExpand = true; + private string ExpandIcon = @Icons.Material.Filled.ArrowForwardIos; + + public List Robots = []; + private RobotInfomationDto? RobotSelected = null; + + public void LoadMaps(List maps, MapInfoDto mapSelected) + { + Maps = maps; + MapSelected = mapSelected; + StateHasChanged(); + } + + public void LoadRobots(List robots) + { + Robots = robots; + if(Robots.Count > 0 && (RobotSelected is null || !Robots.Any(r => r.RobotId == RobotSelected.RobotId))) + { + RobotSelected = Robots.First(); + _ = RobotChanged.InvokeAsync(RobotSelected); + } + StateHasChanged(); + } + + private async Task ExpandedClick() + { + if (isExpand) + { + ExpandIcon = @Icons.Material.Filled.ArrowBackIos; + isExpand = false; + } + else + { + ExpandIcon = @Icons.Material.Filled.ArrowForwardIos; + isExpand = true; + } + await ExpandChanged.InvokeAsync(isExpand); + StateHasChanged(); + } + + private void CheckedClick(MonitorToolbarCheckedType type, bool value) => CheckedEventClick?.Invoke(type, value); +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor.cs b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor.cs new file mode 100644 index 0000000..395dc6e --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/MonitorToolbar.razor.cs @@ -0,0 +1,11 @@ +namespace RobotNet.WebApp.Robots.Components.Monitoring; + +public class MonitorToolBarModel +{ + public bool ShowGrid { get; set; } + public bool ShowName { get; set; } + public bool ShowPath { get; set; } + public bool FocusRobot { get; set; } + public bool ShowLaser { get; set; } + public bool ShowElement { get; set; } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor b/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor new file mode 100644 index 0000000..b684e37 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor.css b/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor.css new file mode 100644 index 0000000..2a68c05 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/OriginVector.razor.css @@ -0,0 +1,4 @@ +line.origin { + stroke-width: var(--origin-vector-stroke-width); + fill: none; +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/RobotCurrentPath.razor b/RobotNet.WebApp/Robots/Components/Monitoring/RobotCurrentPath.razor new file mode 100644 index 0000000..f725de1 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/RobotCurrentPath.razor @@ -0,0 +1,35 @@ + + +@code { + [Parameter] + public bool Show { get; set; } + + private string data = ""; + + private string PathIsNot = "hidden"; + + public void Clear() + { + data = ""; + PathIsNot = "hidden"; + StateHasChanged(); + } + + public void UpdatePath(List path) + { + if (path.Count > 0) + { + var inPath = $"M {path[0].StartX} {path[0].StartY}"; + for (int i = 0; i < path.Count; i++) + { + if (path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}"; + else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}"; + else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}"; + } + data = inPath; + PathIsNot = "visible"; + } + else Clear(); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/RobotLaserScaner.razor b/RobotNet.WebApp/Robots/Components/Monitoring/RobotLaserScaner.razor new file mode 100644 index 0000000..9e28ede --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/RobotLaserScaner.razor @@ -0,0 +1,17 @@ +@foreach (var point in LidarPoints) +{ + +} + +@code { + [Parameter] + public bool ShowLaser { get; set; } + + private List LidarPoints { get; set; } = new(); + + public void SetData(List laser) + { + LidarPoints.Clear(); + LidarPoints.AddRange(laser); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Monitoring/RobotPath.razor b/RobotNet.WebApp/Robots/Components/Monitoring/RobotPath.razor new file mode 100644 index 0000000..5a39c67 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Monitoring/RobotPath.razor @@ -0,0 +1,45 @@ + + + + + + + + + + + + +@code { + [Parameter] + public bool Show { get; set; } + + private string data = ""; + + private string PathIsNot = "hidden"; + + public void Clear() + { + data = ""; + PathIsNot = "hidden"; + StateHasChanged(); + } + + public void UpdatePath(List path) + { + if (path.Count > 0) + { + var inPath = $"M {path[0].StartX} {path[0].StartY}"; + for (int i = 0; i < path.Count; i++) + { + if(path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}"; + else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}"; + else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}"; + } + data = inPath; + PathIsNot = "visible"; + } + else Clear(); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor b/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor new file mode 100644 index 0000000..42b223a --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor @@ -0,0 +1,128 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.MapShares.Models + +@inject IConfiguration Configuration +@inject IJSRuntime JSRuntime +@inject IHttpClientFactory HttpClientFactory + +
+
+ + +
+ + + + +
+
+
+
+ @foreach (var log in SearchLogs) + { +
+ + @log.Time @log.Level + + @log.Message + @if (log.HasException) + { +
+
+                            @log.Exception
+                                
+ } +
+ } +
+
+ + + Loadding... + +
+ +@code { + private DateTime DateLog = DateTime.Today; + private bool IsLoading; + private readonly List ShowLogs = new(); + private readonly List SearchLogs = new(); + private string? FilterLog { get; set; } + private ElementReference LogContainerRef { get; set; } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await LoadLogs(true); + } + + private async Task LoadLogs(bool firstRender) + { + try + { + if(!firstRender) + { + IsLoading = true; + ShowLogs.Clear(); + StateHasChanged(); + } + + var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + var logs = await Http.GetFromJsonAsync>($"api/RobotManagerLogger/open-acs?date={DateLog}"); + ShowLogs.AddRange(logs ?? []); + + IsLoading = false; + StateHasChanged(); + + await ReloadLogs(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task ReloadLogs() + { + IsLoading = true; + SearchLogs.Clear(); + StateHasChanged(); + + foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000)) + { + try + { + var log = System.Text.Json.JsonSerializer.Deserialize(line); + if (log is not null) SearchLogs.Add(log); + } + catch (System.Text.Json.JsonException) + { + continue; + } + } + + IsLoading = false; + StateHasChanged(); + await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef); + } + + private async Task OnSearch(string text) + { + FilterLog = text; + await ReloadLogs(); + } + + private async Task OnDateChanged(DateTime? date) + { + if (date is not null && date.HasValue) + { + DateLog = date.Value; + await LoadLogs(false); + } + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor.css b/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor.css new file mode 100644 index 0000000..22ddec5 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/ACSLog.razor.css @@ -0,0 +1,57 @@ +.data-preview { + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + border: 1px solid silver; + border-radius: 5px; + position: relative; +} + + .data-preview .content { + display: flex; + font-size: 13px; + overflow-x: hidden; + overflow-y: auto; + flex-grow: 1; + padding: 5px; + position: relative; + } + + .data-preview .content.log-container { + display: flex; + height: 100%; + width: 100%; + position: absolute; + top: 0px; + left: 0px; + padding: 10px; + flex-direction: column; + } + +.log { + word-wrap: break-word; + line-height: 18px; + margin-bottom: 5px; +} + +.log-logger { + color: rgba(0, 0, 0, 0.3); + font-size: 12px; +} + +.log-level { + display: inline-block; + width: 46px; +} + +.log-head { + border-radius: 3px; + padding: 2px 5px; +} + +.log-exception { + line-height: 16px; + margin-left: 30px; + color: crimson; +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/DashboardConfig.razor b/RobotNet.WebApp/Robots/Components/OpenACS/DashboardConfig.razor new file mode 100644 index 0000000..9ce1d4d --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/DashboardConfig.razor @@ -0,0 +1,111 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory + + + + Dashboard Mission Names + + +
+
+ + Add +
+
+ @foreach(var missionName in MissionNames) + { +
+ + @missionName + +
+ } +
+
+
+ Update +
+
+
+
+ + + + +
+ +@code { + private List MissionNames = []; + private bool OverlayIsVisible; + + private string MissionName = ""; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await LoadMisison(); + } + + private async Task LoadMisison() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + using var client = HttpClientFactory.CreateClient("ScriptManagerAPI"); + var mission = await client.GetFromJsonAsync>("api/DashboardConfig"); + if (mission is null || mission.Data is null || !mission.IsSuccess) MissionNames = []; + else MissionNames = [.. mission.Data]; + + OverlayIsVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task Update() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("ScriptManagerAPI"); + + var result = await (await Http.PostAsJsonAsync($"api/DashboardConfig", MissionNames)).Content.ReadFromJsonAsync(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Cập nhật không thành công", Severity.Warning); return; } + + OverlayIsVisible = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private void Add() + { + if (string.IsNullOrEmpty(MissionName)) + { + Snackbar.Add("Vui lòng nhập mission name", Severity.Warning); + return; + } + if (MissionNames.Contains(MissionName)) + { + Snackbar.Add("Mission name đã tồn tại", Severity.Warning); + return; + } + MissionNames.Add(MissionName); + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/PublishSetting.razor b/RobotNet.WebApp/Robots/Components/OpenACS/PublishSetting.razor new file mode 100644 index 0000000..fa2154e --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/PublishSetting.razor @@ -0,0 +1,128 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.RobotShares.OpenACS + +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + + + + Publish Setting + + +
+ + + + +
+
+ Update +
+
+
+
+ + + + +
+ +@code { + [Parameter, EditorRequired] + public OpenACSPublishSettingDto SettingDto { get; set; } = new(false, "", [], 2000); + + private OpenACSPublishSettingDto NewSettingDto { get; set; } = new(false, "", [], 2000); + private bool OverlayIsVisible; + + + public void UpdateSettings(OpenACSPublishSettingDto settings) + { + SettingDto = settings; + NewSettingDto = new() + { + IsPublishEnabled = SettingDto.IsPublishEnabled, + PublishInterval = SettingDto.PublishInterval, + PublishUrl = SettingDto.PublishUrl, + PublishUrlsUsed = SettingDto.PublishUrlsUsed, + }; + StateHasChanged(); + } + + private async Task> UrlSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return SettingDto.PublishUrlsUsed; + } + + return SettingDto.PublishUrlsUsed.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)); + } + + private async Task Update() + { + try + { + if (NewSettingDto.PublishUrl == SettingDto.PublishUrl && NewSettingDto.PublishInterval == SettingDto.PublishInterval) + { + Snackbar.Add("Không có thay đổi nào để cập nhật", Severity.Warning); + return; + } + + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + + var result = await (await Http.PostAsJsonAsync($"api/OpenACSSettings/publish", new OpenACSPublishSettingModel(NewSettingDto.PublishUrl, NewSettingDto.PublishInterval))).Content.ReadFromJsonAsync>(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Cập nhật không thành công", Severity.Warning); return; } + else if (result.Data == null) + { + Snackbar.Add("Lỗi dữ liệu trả về", Severity.Warning); + return; + } + SettingDto = result.Data; + NewSettingDto = new() + { + IsPublishEnabled = SettingDto.IsPublishEnabled, + PublishInterval = SettingDto.PublishInterval, + PublishUrl = SettingDto.PublishUrl, + PublishUrlsUsed = SettingDto.PublishUrlsUsed, + }; + + OverlayIsVisible = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task EnableChanged() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + var result = await Http.GetFromJsonAsync($"api/OpenACSSettings/publish?enable={NewSettingDto.IsPublishEnabled}"); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Cập nhật không thành công", Severity.Warning); return; } + + SettingDto.IsPublishEnabled = NewSettingDto.IsPublishEnabled; + + OverlayIsVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor new file mode 100644 index 0000000..d45c8b0 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor @@ -0,0 +1,59 @@ +@using RobotNet.RobotShares.OpenACS + +@inject IHttpClientFactory HttpFactory +@inject ISnackbar Snackbar + +
+
+ + @foreach(var robot in RobotACSLockedDto) + { + @robot.RobotId + } + + +
+ + + +
+
+
+ @if(RobotSeleced is not null) + { + foreach(var zone in RobotSeleced.ZoneIds) + { +
+ @zone +
+ } + } +
+
+ +@code { + private RobotACSLockedDto[] RobotACSLockedDto = []; + private RobotACSLockedDto? RobotSeleced = null; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + await LoadRobots(); + } + + private async Task LoadRobots() + { + RobotACSLockedDto = []; + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var robots = await Http.GetFromJsonAsync("api/TrafficACSRequest"); + if (robots is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (robots.Length > 0) + { + RobotACSLockedDto = [..robots.OrderByDescending(robot => robot.RobotId)]; + RobotSeleced = RobotACSLockedDto.First(); + } + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor.css b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor.css new file mode 100644 index 0000000..74746cd --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficACSLockedView.razor.css @@ -0,0 +1,29 @@ +.view-paper { + display: flex; + width: 100%; + height: 100%; + border: 1px solid silver; + border-radius: 5px; + position: relative; + flex-direction: column; +} + + .view-paper .content { + display: flex; + overflow-x: hidden; + overflow-y: auto; + flex-grow: 1; + padding: 5px; + position: relative; + } + + .view-paper .content .zone { + margin: 5px; + border: 1px solid silver; + padding: 5px; + height: fit-content; + width: fit-content; + font-size: 13px; + border-radius: 5px; + flex-wrap: wrap; + } \ No newline at end of file diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/TrafficRequest.razor b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficRequest.razor new file mode 100644 index 0000000..c24c268 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficRequest.razor @@ -0,0 +1,104 @@ +@using RobotNet.RobotShares.OpenACS +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpClientFactory + + + + Traffic Request + + +
+ + + + + @foreach (TrafficRequestType item in Enum.GetValues(typeof(TrafficRequestType))) + { + + } + +
+
+ Request +
+
+
+
+ + + + +
+ +@code { + private bool OverlayIsVisible; + + private RobotDto[] Robots = []; + private string RobotId = string.Empty; + private string ZoneId = string.Empty; + private TrafficRequestType Type = TrafficRequestType.OUT; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + await LoadRobots(); + } + + private async Task LoadRobots() + { + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + var robots = await Http.GetFromJsonAsync>>("api/Robots"); + if (robots is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!robots.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {robots.Message}", Severity.Error); + else if (robots.Data is not null && robots.Data.Any()) + { + Robots = [..robots.Data]; + } + OverlayIsVisible = false; + StateHasChanged(); + } + + + private async Task> UrlSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Robots.Select(n => n.Name); + } + + return Robots.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(n => n.Name); + } + + private async Task Request() + { + if(string.IsNullOrEmpty(RobotId)) + { + Snackbar.Add($"Trường thông tin Robot Id không được để trống", Severity.Warning); + return; + } + if (string.IsNullOrEmpty(ZoneId)) + { + Snackbar.Add($"Trường thông tin Zone Id không được để trống", Severity.Warning); + return; + } + + OverlayIsVisible = true; + StateHasChanged(); + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + var request = await (await Http.PostAsJsonAsync("api/TrafficACSRequest", new TrafficACSRequestModel() { RobotId = RobotId, ZoneId = ZoneId, Type = Type})).Content.ReadFromJsonAsync>(); + if (request is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); + else if (!request.IsSuccess) Snackbar.Add($"Có lỗi xảy ra: {request.Message}", Severity.Warning); + else if (!request.Data) Snackbar.Add("ACS trả về không thành công thành công", Severity.Warning); + else if (request.Data) Snackbar.Add($"{(string.IsNullOrEmpty(request.Message) ? "Yêu cầu thành công" : request.Message)}", Severity.Success); + + OverlayIsVisible = false; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/OpenACS/TrafficSetting.razor b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficSetting.razor new file mode 100644 index 0000000..154a234 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/OpenACS/TrafficSetting.razor @@ -0,0 +1,124 @@ +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.RobotShares.OpenACS + +@inject IHttpClientFactory HttpClientFactory +@inject ISnackbar Snackbar + + + + Traffic Setting + + +
+ + + +
+
+ Update +
+
+
+
+ + + + +
+ +@code { + [Parameter, EditorRequired] + public OpenACSTrafficSettingDto SettingDto { get; set; } = new(false, "", []); + + private OpenACSTrafficSettingDto NewSettingDto { get; set; } = new(false, "", []); + private bool OverlayIsVisible; + + public void UpdateSettings(OpenACSTrafficSettingDto settings) + { + SettingDto = settings; + NewSettingDto = new() + { + IsTrafficEnabled = SettingDto.IsTrafficEnabled, + TrafficUrl = SettingDto.TrafficUrl, + PublishUrlsUsed = SettingDto.PublishUrlsUsed, + }; + StateHasChanged(); + } + + private async Task> UrlSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return SettingDto.PublishUrlsUsed; + } + + return SettingDto.PublishUrlsUsed.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase)); + } + + private async Task Update() + { + try + { + if (NewSettingDto.TrafficUrl == SettingDto.TrafficUrl) + { + Snackbar.Add("Không có thay đổi nào để cập nhật", Severity.Warning); + return; + } + + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + + var result = await (await Http.PostAsJsonAsync($"api/OpenACSSettings/traffic", new OpenACSTrafficSettingModel(NewSettingDto.TrafficUrl))).Content.ReadFromJsonAsync>(); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Cập nhật không thành công", Severity.Warning); return; } + else if (result.Data == null) + { + Snackbar.Add("Lỗi dữ liệu trả về", Severity.Warning); + return; + } + SettingDto = result.Data; + NewSettingDto = new() + { + IsTrafficEnabled = SettingDto.IsTrafficEnabled, + TrafficUrl = SettingDto.TrafficUrl, + PublishUrlsUsed = SettingDto.PublishUrlsUsed, + }; + + OverlayIsVisible = false; + Snackbar.Add("Cập nhật thành công", Severity.Success); + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task EnableChanged() + { + try + { + OverlayIsVisible = true; + StateHasChanged(); + + using var Http = HttpClientFactory.CreateClient("RobotManagerAPI"); + var result = await Http.GetFromJsonAsync($"api/OpenACSSettings/traffic?enable={NewSettingDto.IsTrafficEnabled}"); + if (result is null) { Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Warning); return; } + else if (!result.IsSuccess) { Snackbar.Add(result.Message ?? "Cập nhật không thành công", Severity.Warning); return; } + + SettingDto.IsTrafficEnabled = NewSettingDto.IsTrafficEnabled; + + OverlayIsVisible = false; + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } +} diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotAction.razor b/RobotNet.WebApp/Robots/Components/Robot/RobotAction.razor new file mode 100644 index 0000000..f3538f3 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotAction.razor @@ -0,0 +1,310 @@ +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.WebApp.Clients + +@inject RobotHubClient RobotHub +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpFactory + + + + Setting + + @Robot.Name + + +
+
+
+ + + + No items found + + + +
+ SetMap +
+
+
+ + + + No items found + + + + Move +
+ CancelOrder +
+
+
+ + + + No items found + + + + Send +
+ CancelAction +
+
+
+ + + +
+ InitPose +
+
+
+ + + + +
+ +@code { + [Parameter, EditorRequired] + public RobotDto Robot { get; set; } = new(); + + private MapInfoDto MapSelected = default!; + private string SelectedNode = ""; + + private List Nodes = []; + private List Maps = []; + + private (double X, double Y, double Theta) InitPosition = new(); + private bool IsWorking = false; + private bool OverlayIsVisible; + + private List Actions = []; + private ActionDto? ActionSelected = null; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await RobotHub.StartAsync(); + + await LoadMaps(); + } + + public void UpdateState(bool isOnline, bool isWorking) + { + Robot.Online = isOnline; + IsWorking = isWorking; + StateHasChanged(); + } + + private async Task MoveToNode() + { + if (string.IsNullOrEmpty(SelectedNode)) + { + Snackbar.Add("Vui lòng chọn node", Severity.Warning); + } + else + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.MoveToNode(Robot.RobotId, SelectedNode); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + } + StateHasChanged(); + } + + private async Task CancelOrder() + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.CancelNavigation(Robot.RobotId); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + StateHasChanged(); + } + + private async Task SetMap() + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.SetMap(Robot.RobotId, MapSelected.Id); + if (result.IsSuccess) + { + Robot.MapId = MapSelected.Id; + await LoadNodes(); + await LoadActionAsync(); + Snackbar.Add("Ra lệnh thành công", Severity.Success); + } + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + StateHasChanged(); + } + + private async Task InitPose() + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.SetInitialPose(Robot.RobotId, InitPosition.X, InitPosition.Y, InitPosition.Theta * Math.PI / 180); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + StateHasChanged(); + } + + private async Task SendAction() + { + if (ActionSelected is null) + { + Snackbar.Add("Vui lòng chọn action", Severity.Warning); + } + else + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.SendAction(Robot.RobotId, ActionSelected); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + } + StateHasChanged(); + } + + private async Task CancelAction() + { + OverlayIsVisible = true; + StateHasChanged(); + + var result = await RobotHub.CancelAction(Robot.RobotId); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + + OverlayIsVisible = false; + StateHasChanged(); + } + + public async Task LoadActionAsync() + { + Actions.Clear(); + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var result = await Http.GetFromJsonAsync>($"api/Actions/{Robot.MapId}"); + + if (result is not null && result.Any()) + { + Actions.AddRange(result); + } + if (Actions.Any()) ActionSelected = Actions.First(); + + StateHasChanged(); + } + + public async Task LoadNodes() + { + try + { + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var result = await Http.GetFromJsonAsync($"api/Nodes/{Robot.MapId}"); + if (result is not null) Nodes = result.Where(n => !string.IsNullOrEmpty(n.Name)).ToList(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task LoadMaps() + { + try + { + Maps.Clear(); + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var maps = await Http.GetFromJsonAsync>($"api/MapsManager?txtSearch="); + Maps.AddRange(maps ?? []); + StateHasChanged(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + return; + } + } + + private async Task> MapSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Maps; + } + + return Maps.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)); + } + + private async Task> NodeSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Nodes.Select(n => n.Name); + } + + return Nodes.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(n => n.Name); + } + + + private async Task> ActionSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Actions; + } + + return Actions.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)); + } + + private void NodeChanged(string value) + { + if (string.IsNullOrEmpty(SelectedNode) || value != SelectedNode) SelectedNode = value; + } + + + private void ActionChanged(ActionDto value) + { + if (ActionSelected is null || value.Id != ActionSelected.Id) ActionSelected = value; + } + + private void MapChanged(MapInfoDto value) + { + if (MapSelected is null || value.Id != MapSelected.Id) MapSelected = value; + } + + public async ValueTask DisposeAsync() + { + await RobotHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor b/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor new file mode 100644 index 0000000..9529496 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor @@ -0,0 +1,129 @@ +@using System.Text.Json +
+
+ + + + + + + + + + +
+
+ @DataPreview +
+
+ + + +@code { + private DataPreviewType DataType; + private string DataPreview = ""; + + private RobotVDA5050StateDto RobotState = new(); + + public void UpdateState(RobotVDA5050StateDto robotstate) + { + RobotState = robotstate; + DataTypeChanged(); + StateHasChanged(); + } + + private void DataTypeChanged() + { + switch (DataType) + { + case DataPreviewType.Visualization: + DataPreview = JsonSerializer.Serialize(RobotState.Visualization, JsonOptionExtends.Read); + break; + case DataPreviewType.Node: + DataPreview = JsonSerializer.Serialize(RobotState.State.NodeStates, JsonOptionExtends.Read); + break; + case DataPreviewType.Edge: + DataPreview = JsonSerializer.Serialize(RobotState.State.EdgeStates, JsonOptionExtends.Read); + break; + case DataPreviewType.Order: + DataPreview = JsonSerializer.Serialize(GetOrderState(RobotState), JsonOptionExtends.Read); + break; + case DataPreviewType.Action: + DataPreview = JsonSerializer.Serialize(RobotState.State.ActionStates, JsonOptionExtends.Read); + break; + case DataPreviewType.Infomation: + DataPreview = JsonSerializer.Serialize(RobotState.State.Information, JsonOptionExtends.Read); + break; + case DataPreviewType.Battery: + DataPreview = JsonSerializer.Serialize(RobotState.State.BatteryState, JsonOptionExtends.Read); + break; + case DataPreviewType.Error: + DataPreview = JsonSerializer.Serialize(RobotState.State.Errors, JsonOptionExtends.Read); + break; + }; + StateHasChanged(); + } + + private OrderState GetOrderState(RobotVDA5050StateDto state) + { + return new() + { + OrderId = state.State.OrderId, + OrderUpdateId = state.State.OrderUpdateId, + ZoneSetId = state.State.ZoneSetId, + LastNodeId = state.State.LastNodeId, + LastNodeSequenceId = state.State.LastNodeSequenceId, + Driving = state.State.Driving, + Paused = state.State.Paused, + NewBaseRequest = state.State.NewBaseRequest, + DistanceSinceLastNode = state.State.DistanceSinceLastNode, + OperatingMode = state.State.OperatingMode, + IsCanceled = state.OrderState.IsCanceled, + IsCompleted = state.OrderState.IsCompleted, + IsError = state.OrderState.IsError, + IsProcessing = state.OrderState.IsProcessing, + }; + } + + public enum DataPreviewType + { + Visualization, + Node, + Edge, + Order, + Action, + Infomation, + Battery, + Error + } + + public class OrderState + { + public string OrderId { get; set; } = ""; + public int OrderUpdateId { get; set; } + public string ZoneSetId { get; set; } = ""; + public string LastNodeId { get; set; } = ""; + public int LastNodeSequenceId { get; set; } + public bool Driving { get; set; } + public bool Paused { get; set; } + public bool NewBaseRequest { get; set; } + public double DistanceSinceLastNode { get; set; } + public string OperatingMode { get; set; } = ""; + public bool IsError { get; set; } + public bool IsCompleted { get; set; } + public bool IsProcessing { get; set; } + public bool IsCanceled { get; set; } + } +} diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor.css b/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor.css new file mode 100644 index 0000000..452ea41 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotInfomation.razor.css @@ -0,0 +1,19 @@ +.data-preview { + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + border: 1px solid silver; + border-radius: 5px; +} + + .data-preview .content { + display: flex; + white-space: pre; + overflow-y: auto; + overflow-x: hidden; + font-size: 15px; + flex-grow: 1; + border-top: 1px solid silver; + padding: 5px; + } diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor b/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor new file mode 100644 index 0000000..d635b89 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor @@ -0,0 +1,160 @@ +@using System.Net.Http.Headers +@inject IHttpClientFactory HttpFactory +@inject IJSRuntime JSRuntime +@inject ISnackbar Snackbar + +
+ @RobotModelName +
+
+ robot model image +
+
+ Image +
+ + +
+
+
+
+ + + + + Update RobotModel's Image + + + +
+
+
+
+
+ Robot Model Image +
+
+
+
+ + Cancel + Update + +
+@code { + private bool Disable = true; + private string imageSrc = "/images/Image-not-found.png"; + private string RobotModelName = "Robot Model preview"; + private Guid RobotModelId = Guid.Empty; + + private string ImagePreview = ""; + private IBrowserFile? RobotImageChange; + + private bool IsUpdateRobotImageVisable; + + public void SetRobotPreview(RobotModelDto? robotmodel) + { + if (robotmodel is null) + { + Disable = true; + RobotModelName = "Robot Model preview"; + imageSrc = "/images/Image-not-found.png"; + RobotModelId = Guid.Empty; + } + else + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + imageSrc = $"{Http.BaseAddress}api/RobotModels/image/{robotmodel.Id}?t={DateTime.Now}"; + RobotModelName = robotmodel.ModelName; + RobotModelId = robotmodel.Id; + Disable = false; + } + StateHasChanged(); + } + + private async Task DownloadImage() + { + try + { + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var response = await Http.GetAsync(imageSrc); + + if (response.IsSuccessStatusCode) + { + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + + var base64Data = Convert.ToBase64String(fileBytes); + var mimeType = "image/png"; + var url = $"data:{mimeType};base64,{base64Data}"; + + await JSRuntime.InvokeVoidAsync("DownloadImage", url, $"{RobotModelName}.png"); + } + else Snackbar.Add("Không thể tải ảnh robot", Severity.Warning); + } + catch (Exception ex) + { + Snackbar.Add($"Không thể tải ảnh robot: {ex.Message}", Severity.Warning); + } + } + + private void OpenUpdateRobotImage() + { + ImagePreview = imageSrc; + RobotImageChange = null; + IsUpdateRobotImageVisable = true; + StateHasChanged(); + } + + public async Task UploadMedia(IBrowserFile file) + { + var path = Path.Combine(Path.GetTempPath(), RobotModelName); + await using var fs = new FileStream(path, FileMode.Create); + await file.OpenReadStream(file.Size).CopyToAsync(fs); + var bytes = new byte[file.Size]; + fs.Position = 0; + await fs.ReadAsync(bytes); + fs.Close(); + File.Delete(path); + return $"data:{file.ContentType};base64,{Convert.ToBase64String(bytes)}"; + } + + private async Task RobotImageChanged(InputFileChangeEventArgs e) + { + RobotImageChange = e.File; + if (RobotImageChange is not null) + { + ImagePreview = await UploadMedia(RobotImageChange); + StateHasChanged(); + } + } + + private async Task UpdateRobotModelImage() + { + if (RobotImageChange is null) return; + + using var content = new MultipartFormDataContent(); + var fileContent = new StreamContent(RobotImageChange.OpenReadStream(maxAllowedSize: 20480000)); + fileContent.Headers.ContentType = new MediaTypeHeaderValue(RobotImageChange.ContentType); + content.Add(fileContent, "image", RobotImageChange.Name); + + using var Http = HttpFactory.CreateClient("RobotManagerAPI"); + var result = await (await Http.PutAsync($"api/RobotModels/image/{RobotModelId}", content)).Content.ReadFromJsonAsync(); + if (result is null) Snackbar.Add("Lỗi giao tiếp với hệ thống", Severity.Error); + else if (!result.IsSuccess) Snackbar.Add(result.Message ?? "Lỗi chưa xác định.", Severity.Error); + else + { + imageSrc = ImagePreview; + IsUpdateRobotImageVisable = false; + Snackbar.Add("Thay đổi thành công", Severity.Success); + } + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor.css b/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor.css new file mode 100644 index 0000000..5e0993b --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotModelPreview.razor.css @@ -0,0 +1,43 @@ +.robot-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .robot-item img { + justify-content: center; + max-width: 300px; + width: 95%; + height: 400px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } + +.title { + display: flex; + height: 77px; + width: 100%; + justify-content: center; + align-content: center; + /*border-bottom: 0.5px solid gray;*/ + font-size: 30px; + flex-wrap: wrap; + padding-bottom: .5rem; +} + +.robot-update-item { + display: flex; + justify-content: center; + width: 100%; + height: fit-content; +} + + .robot-update-item img { + width: 400px; + height: 300px; + object-fit: contain; + border-radius: 10px; + image-rendering: pixelated; + } diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor b/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor new file mode 100644 index 0000000..66f8f07 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor @@ -0,0 +1,147 @@ +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@using RobotNet.WebApp.Clients + +@inject RobotHubClient RobotHub +@inject ISnackbar Snackbar +@inject IHttpClientFactory HttpFactory + + + + Test + + +
+
+
+ @* + + + No items found + + + *@ + 1 ? "s have" : " has")} been selected")"> + @foreach (var node in Nodes) + { + @node.Name + } + +
+
+ Add +
+
+
+ @foreach (var node in SelectedNodes) + { +
+ + @(node.Name) + +
+ } +
+
+ Run + Cancel +
+
+
+
+ +@code { + [Parameter, EditorRequired] + public RobotDto Robot { get; set; } = new(); + + // private NodeDto? SelectedNode; + + private List Nodes = []; + private bool IsWorking = false; + private List SelectedNodes = []; + private IEnumerable PreSelectedNodes = new HashSet(); + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (!firstRender) return; + + await RobotHub.StartAsync(); + } + + public void UpdateState(bool isOnline, bool isWorking) + { + Robot.Online = isOnline; + IsWorking = isWorking; + StateHasChanged(); + } + + public async Task LoadNodes() + { + try + { + using var Http = HttpFactory.CreateClient("MapManagerAPI"); + var result = await Http.GetFromJsonAsync($"api/Nodes/{Robot.MapId}"); + if (result is not null) Nodes = result.Where(n => !string.IsNullOrEmpty(n.Name)).ToList(); + } + catch (AccessTokenNotAvailableException ex) + { + ex.Redirect(); + } + } + + private async Task> NodeSearch(string value, CancellationToken token) + { + await Task.Delay(5, token); + + if (string.IsNullOrEmpty(value)) + { + return Nodes.Select(n => n); + } + + return Nodes.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase)).Select(n => n); + } + + // private void NodeChanged(List value) + // { + // if (SelectedNode is null || value.Id != SelectedNode.Id) SelectedNode = value; + // } + + private void AddNode() + { + // if (SelectedNode is not null && !SelectedNodes.Contains(SelectedNode)) + // { + // SelectedNodes.Add(SelectedNode); + // StateHasChanged(); + // } + if (PreSelectedNodes.Any()) + { + SelectedNodes.Clear(); + SelectedNodes.AddRange(PreSelectedNodes); + StateHasChanged(); + } + } + + private async Task Cancel() + { + var result = await RobotHub.CancelNavigation(Robot.RobotId); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + StateHasChanged(); + } + + private async Task Run() + { + var result = await RobotHub.MoveRandom(Robot.RobotId, [.. SelectedNodes.Select(n => n.Name)]); + if (result.IsSuccess) Snackbar.Add("Ra lệnh thành công", Severity.Success); + else Snackbar.Add($"Ra lệnh không thành công: {result.Message}", Severity.Warning); + StateHasChanged(); + } + + public async ValueTask DisposeAsync() + { + await RobotHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor.css b/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor.css new file mode 100644 index 0000000..e02c9ca --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Robot/RobotTestRandom.razor.css @@ -0,0 +1,13 @@ +.paper-nodes { + padding: 5px; + border: 1px solid silver; + border-radius: 10px; + display: flex; + flex-wrap: wrap; + align-content: flex-start; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + max-width: 530px; + height: 200px; +} diff --git a/RobotNet.WebApp/Robots/Components/Traffic/TrafficAgentReview.razor b/RobotNet.WebApp/Robots/Components/Traffic/TrafficAgentReview.razor new file mode 100644 index 0000000..c0d0903 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Traffic/TrafficAgentReview.razor @@ -0,0 +1,123 @@ +
+ + + +
+ LockedNodes + + + +
+
+ +
+ @foreach (var node in Agent.LockedNodes) + { +
+ +
+ } +
+
+
+
+ + + + +
+ Path + + + +
+
+ +
+ @foreach (var node in Agent.Nodes) + { +
+ +
+ } +
+
+
+
+ + + + +
+ SubNodes + + + +
+
+ +
+ @foreach (var node in Agent.SubNodes) + { +
+ +
+ } +
+
+
+
+ + + + +
+ GiveWayNodes + + + +
+
+ +
+ @foreach (var node in Agent.GiveWayNodes) + { +
+ +
+ } +
+
+
+
+ + + + +
+ Conflict Agent +
+
+ + Conflict Agent: @Agent.ConflictAgentId + Conflict Node: @(Agent.ConflictNode is null ? "" : string.IsNullOrEmpty(Agent.ConflictNode.Name) ? Agent.ConflictNode.Id.ToString("N").Substring(0, 4) : Agent.ConflictNode.Name) + +
+
+
+@code { + private TrafficAgentDto Agent { get; set; } = new(); + + public void UpdateState(TrafficAgentDto agent) + { + Agent.Nodes = [.. agent.Nodes]; + Agent.ReleaseNode = agent.ReleaseNode; + Agent.LockedNodes = [.. agent.LockedNodes]; + Agent.InNode = agent.InNode; + Agent.SubNodes = [.. agent.SubNodes]; + Agent.GiveWayNodes = [.. agent.GiveWayNodes]; + Agent.ConflictNode = agent.ConflictNode; + Agent.ConflictAgentId = agent.ConflictAgentId; + StateHasChanged(); + } +} diff --git a/RobotNet.WebApp/Robots/Components/Traffic/TrafficNodePreview.razor b/RobotNet.WebApp/Robots/Components/Traffic/TrafficNodePreview.razor new file mode 100644 index 0000000..32ca34a --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/Traffic/TrafficNodePreview.razor @@ -0,0 +1,15 @@ + + + @(string.IsNullOrEmpty(Node.Name) ? Node.Id.ToString("N").Substring(0,4) : Node.Name) + + + ID: @Node.Id + X: @Node.X + Y: @Node.Y + + + +@code { + [Parameter, EditorRequired] + public TrafficNodeDto Node { get; set; } = new(); +} diff --git a/RobotNet.WebApp/Robots/Components/_Imports.razor b/RobotNet.WebApp/Robots/Components/_Imports.razor new file mode 100644 index 0000000..576a8e6 --- /dev/null +++ b/RobotNet.WebApp/Robots/Components/_Imports.razor @@ -0,0 +1,7 @@ +@using RobotNet.MapShares +@using RobotNet.MapShares.Dtos +@using RobotNet.MapShares.Enums +@using RobotNet.RobotShares.Dtos +@using RobotNet.RobotShares.Enums +@using RobotNet.RobotShares.Models +@using RobotNet.Shares diff --git a/RobotNet.WebApp/Scripts/Components/ActionButton.razor b/RobotNet.WebApp/Scripts/Components/ActionButton.razor new file mode 100644 index 0000000..524d176 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ActionButton.razor @@ -0,0 +1,31 @@ + + + + +@code { + [Parameter] + public string Icon { get; set; } = "mdi-alpha"; + + [Parameter] + public string Tooltip { get; set; } = ""; + + [Parameter] + public bool Large { get; set; } + + [Parameter] + public bool Disabled { get; set; } + + [Parameter] + public EventCallback OnClick { get; set; } + + public string Class => $"{(Large ? "large" : "")} {(Disabled ? "disabled" : "")}"; + + private async Task Click() + { + if (Disabled) return; + + await OnClick.InvokeAsync(); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/ActionButton.razor.css b/RobotNet.WebApp/Scripts/Components/ActionButton.razor.css new file mode 100644 index 0000000..befa217 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ActionButton.razor.css @@ -0,0 +1,31 @@ +button { + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + font-size: 20px; + border-radius: 3px; + margin-left: 5px; + color: white; +} + + button:not(.disabled):hover { + background-color: rgba(255,255,255,0.3); + } + + button:not(.disabled):active { + background-color: transparent; + } + + button.disabled > span.mdi { + color: rgba(255,255,255,0.3) !important; + } + + button.large { + width: 32px; + height: 32px; + font-size: 28px; + border-radius: 6px; + } diff --git a/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor b/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor new file mode 100644 index 0000000..db19694 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor @@ -0,0 +1,34 @@ +@implements IDisposable + +@using Microsoft.CodeAnalysis + +
+    @this.Model.Message
+
+ +@code { + [Parameter, EditorRequired] + public ConsoleItemModel Model { get; set; } = null!; + + string Class => this.Model.Severity switch + { + DiagnosticSeverity.Info => "console-info", + DiagnosticSeverity.Warning => "console-warning", + DiagnosticSeverity.Error => "console-error", + _ => "console-none" + }; + + public void Dispose() + { + Model.Changed -= StateHasChanged; + } + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if(firstRender) + { + Model.Changed += StateHasChanged; + } + } +} diff --git a/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor.css b/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor.css new file mode 100644 index 0000000..70da144 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ConsoleItem.razor.css @@ -0,0 +1,21 @@ +.console-none { + display: none; +} + +.console-info { + color: grey; + margin: 0px; + text-wrap: auto; +} + +.console-warning { + color: yellow; + margin: 0px; + text-wrap: auto; +} + +.console-error { + color: red; + margin: 0px; + text-wrap: auto; +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/InstantiateMissionDialog.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/InstantiateMissionDialog.razor new file mode 100644 index 0000000..57e5fea --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/InstantiateMissionDialog.razor @@ -0,0 +1,216 @@ +@using RobotNet.Shares +@using RobotNet.Clients +@using RobotNet.WebApp.Scripts.Models + +@inject ISnackbar Snackbar +@inject IHttpClientFactory httpFactory + + + + + @Title + + + +
+ @foreach (var param in Model.Parameters) + { +
+
+ @param.Name : +
+
+ @switch (param.Type) + { + case "System.Boolean": + + break; + case "System.Byte": + + break; + case "System.SByte": + + break; + case "System.Int16": + + break; + case "System.UInt16": + + break; + case "System.Int32": + + break; + case "System.UInt32": + + break; + case "System.Int64": + + break; + case "System.UInt64": + + break; + case "System.Single": + + break; + case "System.Double": + + break; + case "System.Decimal": + + break; + case "System.String": + + break; + case "System.Char": + + break; + case "System.Threading.CancellationToken": + + <CancellationToken> + + break; + default: + Unsupport parameter with type @param.Type + break; + } +
+
+ } +
+
+ + Cancel + Instantiate + +
+ +@code { + [CascadingParameter] + private IMudDialogInstance Dialog { get; set; } = null!; + + [Parameter, EditorRequired] + public ScriptMissionModel Model { get; set; } = default!; + + public string Title => $"Instantiate Mission {Model.Name}"; + private HttpClient http = default!; + + private bool IsInstantiating = false; + + protected override void OnInitialized() + { + base.OnInitialized(); + http = httpFactory.CreateClient("ScriptManagerAPI"); + } + + private void OnCancel() => Dialog.Cancel(); + + private async Task Instantiate() + { + if (IsInstantiating) return; + IsInstantiating = true; + StateHasChanged(); // Force UI update to reflect instantiation state + + var parameters = Model.Parameters + .Where(p => p.Type != "RobotNet.Script.IRobot") + .ToDictionary(p => p.Name, p => p.ToString()); + + var result = await http.PostFromJsonAsync>("api/ScriptMissions/Runner", new InstanceMissionCreateModel(Model.Name, parameters)); + if (result is null) + { + Snackbar.Add("Failed to instantiate mission: server response null", Severity.Error); + } + else if (result.IsSuccess) + { + Snackbar.Add($"Mission {Model.Name} instantiated successfully with ID: {result.Data}", Severity.Success); + Dialog.Close(DialogResult.Ok(Model.Name)); + } + else + { + Snackbar.Add($"Failed to instantiate mission: {result.Message}", Severity.Error); + } + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/Missions.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/Missions.razor new file mode 100644 index 0000000..01a7417 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/Missions.razor @@ -0,0 +1,259 @@ +@implements IDisposable + +@using Microsoft.AspNetCore.SignalR.Client +@using RobotNet.WebApp.Scripts.Models + +@inject IHttpClientFactory httpFactory +@inject IDialogService DialogService + +
+
+ + +
+

Danh sách Script Missions

+ +
+
+ + + + + + + + + STT + Tên + Parameters + Script + + + @context.Index + @context.Name + + + @if (context.Parameters?.Any() == true) + { + + } + else + { + Không có + } + + + + + + + + + + + + + +
+
+
+ + + +
Thông tin Parameters
+ @if (_selectedParameters != null) + { + + + Tên + Kiểu + Default + + + @context.Name + @context.Type + @context.Default + + + } +
+ + Đóng + +
+ + + +
Mã Script đầy đủ
+ +
@_selectedCode
+
+
+ + Đóng + +
+ +@code { + [CascadingParameter] + private ProcessorHubClient ProcessorHub { get; set; } = null!; + + private string TableHeight = "0px"; + private ElementReference divRef; + private ElementReference toolbarRef; + + private HttpClient http = default!; + private List scriptMissions = []; + + // Dialog state + private bool _parametersDialogOpen = false; + private IEnumerable? _selectedParameters; + + private bool _codeDialogOpen = false; + private string? _selectedCode; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + var rect = await divRef.MudGetBoundingClientRectAsync(); + var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync(); + TableHeight = $"{rect.Height - 25 - Math.Max(toolbarRect.Height, 64)}px"; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + http = httpFactory.CreateClient("ScriptManagerAPI"); + await ReloadData(); + + if (ProcessorHub.IsConnected) + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + } + ProcessorHub.StateChanged += OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged += OnConnectionStateChanged; + } + + + private void OnConnectionStateChanged(HubConnectionState state) + { + if (state == HubConnectionState.Connected) + { + _ = InvokeAsync(async Task () => + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + }); + + } + } + + private bool isRebuild = false; + private bool CallDisabled = true; + private void OnProcessorStateChanged(ProcessorState state) + { + if (state == ProcessorState.Building) + { + isRebuild = true; + } + else if (state == ProcessorState.Ready) + { + if (isRebuild) + { + isRebuild = false; + _ = InvokeAsync(async Task () => + { + await ReloadData(); + }); + } + } + + if (CallDisabled) + { + if (state == ProcessorState.Running) + { + CallDisabled = false; + StateHasChanged(); + } + } + else + { + if (state != ProcessorState.Running) + { + CallDisabled = true; + StateHasChanged(); + } + } + } + + private async Task ReloadData() + { + scriptMissions.Clear(); + var missions = await http.GetFromJsonAsync>("/api/ScriptMissions") ?? []; + foreach (var item in missions.Select((value, index) => (value, index))) + { + scriptMissions.Add(new ScriptMissionModel(item.index + 1, item.value)); + } + StateHasChanged(); + } + + + private void ShowParametersDialog(ScriptMissionModel model) + { + _selectedParameters = model.Parameters; + _parametersDialogOpen = true; + StateHasChanged(); + } + + private void CloseParametersDialog() + { + _parametersDialogOpen = false; + _selectedParameters = null; + StateHasChanged(); + } + + private void ShowCodeDialog(string code) + { + _selectedCode = code; + _codeDialogOpen = true; + StateHasChanged(); + } + + private void CloseCodeDialog() + { + _codeDialogOpen = false; + _selectedCode = null; + StateHasChanged(); + } + + private async Task ShowInstantiateMissionDialog(ScriptMissionModel model) + { + if (CallDisabled) return; + + foreach (var parameter in model.Parameters) + { + parameter.Reset(); + } + ; + var parameters = new DialogParameters() + { + {x => x.Model, model}, + }; + + var dialog = await DialogService.ShowAsync("Instantiate new mission", parameters); + var resultDialog = await dialog.Result; + } + + + public void Dispose() + { + ProcessorHub.StateChanged -= OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged -= OnConnectionStateChanged; + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/ProcessController.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/ProcessController.razor new file mode 100644 index 0000000..ecb7daf --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/ProcessController.razor @@ -0,0 +1,127 @@ +@using Microsoft.AspNetCore.SignalR.Client +@using Microsoft.CodeAnalysis +@implements IDisposable + +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +
+ + + + Process Controller - @State + + + + + + + + + + + + + +
+ +@code { + [CascadingParameter] + private ProcessorHubClient ProcessorHub { get; set; } = null!; + + private ProcessorConsole? processorConsole; + public ProcessorState State { get; private set; } = ProcessorState.Idle; + private ProcessorRequest Request = ProcessorRequest.None; + + private bool ResetDisable => State != ProcessorState.Idle && State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError; + private bool BuildDisable => State != ProcessorState.Idle && State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError; + private bool RunDisable => State != ProcessorState.Ready; + private bool StopDisable => State != ProcessorState.Running; + + protected override async Task OnInitializedAsync() + { + if (ProcessorHub.IsConnected) + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + } + + ProcessorHub.StateChanged += OnProcessorStateChanged; + ProcessorHub.RequestChanged += OnRequestChanged; + ProcessorHub.ConnectionStateChanged += OnConnectionStateChanged; + await base.OnInitializedAsync(); + } + + private void OnConnectionStateChanged(HubConnectionState state) + { + if (state == HubConnectionState.Connected) + { + _ = InvokeAsync(async Task () => + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + }); + + } + } + + private void OnProcessorStateChanged(ProcessorState state) + { + if (State == state) return; + + State = state; + StateHasChanged(); + } + + private void OnRequestChanged(ProcessorRequest request) + { + Request = request; + StateHasChanged(); + } + + private async Task Reset() + { + var result = await ProcessorHub.Reset(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to reset processor: {result.Message}", Severity.Error); + } + } + + private async Task Build() + { + var result = await ProcessorHub.Build(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to build processor: {result.Message}", Severity.Error); + } + } + + private async Task Run() + { + var result = await ProcessorHub.Run(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to run processor: {result.Message}", Severity.Error); + } + } + + private async Task Stop() + { + var result = await ProcessorHub.Stop(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to stop processor: {result.Message}", Severity.Error); + } + } + + private void ClearConsole() + { + processorConsole?.Clear(); + } + + public void Dispose() + { + ProcessorHub.StateChanged -= OnProcessorStateChanged; + ProcessorHub.RequestChanged -= OnRequestChanged; + ProcessorHub.ConnectionStateChanged -= OnConnectionStateChanged; + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/SetVariableValueDialog.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/SetVariableValueDialog.razor new file mode 100644 index 0000000..098685a --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/SetVariableValueDialog.razor @@ -0,0 +1,253 @@ +@using RobotNet.Shares +@using RobotNet.Clients +@using RobotNet.WebApp.Scripts.Models + +@inject ISnackbar Snackbar +@inject IHttpClientFactory httpFactory + + + + + @Title + + + +
+
+
+ @Model.Name : +
+
+ @switch (Model.TypeName) + { + case "System.Boolean": + + break; + case "System.Byte": + + break; + case "System.SByte": + + break; + case "System.Int16": + + break; + case "System.UInt16": + + break; + case "System.Int32": + + break; + case "System.UInt32": + + break; + case "System.Int64": + + break; + case "System.UInt64": + + break; + case "System.Single": + + break; + case "System.Double": + + break; + case "System.Decimal": + + break; + case "System.String": + + break; + case "System.Char": + + break; + case "System.Threading.CancellationToken": + + <CancellationToken> + + break; + default: + Unsupport parameter with type @ValueModel.Type + break; + } +
+
+
+
+ + Cancel + Update + +
+ +@code { + [CascadingParameter] + private IMudDialogInstance Dialog { get; set; } = null!; + + [Parameter, EditorRequired] + public ScriptVariableModel Model { get; set; } = default!; + + public string Title => $"Set Variable \"{Model.Name}\""; + private HttpClient http = default!; + + private ScriptValueModel ValueModel = null!; + + private bool IsRequesing = false; + + protected override void OnInitialized() + { + base.OnInitialized(); + ValueModel = new ScriptValueModel(Model.Name, Model.TypeName, ""); + + switch (Model.TypeName) + { + case "System.Boolean": + ValueModel.BoolValue = default; + break; + case "System.Byte": + ValueModel.ByteValue = default; + break; + case "System.SByte": + ValueModel.SByteValue = default; + break; + case "System.Int16": + ValueModel.ShortValue = default; + break; + case "System.UInt16": + ValueModel.UShortValue = default; + break; + case "System.Int32": + ValueModel.IntValue = default; + break; + case "System.UInt32": + ValueModel.UIntValue = default; + break; + case "System.Int64": + ValueModel.LongValue = default; + break; + case "System.UInt64": + ValueModel.ULongValue = default; + break; + case "System.Single": + ValueModel.FloatValue = default; + break; + case "System.Double": + ValueModel.DoubleValue = default; + break; + case "System.Decimal": + ValueModel.DecimalValue = default; + break; + } + http = httpFactory.CreateClient("ScriptManagerAPI"); + } + + private async Task RequestSetValue() + { + if (IsRequesing) return; + IsRequesing = true; + StateHasChanged(); // Force UI update to reflect instantiation state + + var result = await http.PutFromJsonAsync($"/api/ScriptVariables/{ValueModel.Name}", new UpdateVariableModel(ValueModel.Name, ValueModel.ToString())); + if (result == null) + { + Snackbar.Add("Failed to instantiate mission: server response null", Severity.Error); + } + else if (result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Success); + Dialog.Close(); + } + else + { + Snackbar.Add($"Failed to reset variable value: {result.Message}", Severity.Error); + Dialog.Close(); + } + } + + private void OnCancel() => Dialog.Cancel(); +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/Tasks.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/Tasks.razor new file mode 100644 index 0000000..a88f64f --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/Tasks.razor @@ -0,0 +1,263 @@ +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.SignalR.Client +@using RobotNet.WebApp.Scripts.Models + +@inject IHttpClientFactory httpFactory +@inject ScriptTaskHubClient ScriptTaskHub +@inject ISnackbar Snackbar + +
+
+ + +
+

Danh sách Script Tasks

+ +
+
+ + + + + + + + + + Tên + Khoảng thời gian (ms) + Enable + Script + + + @context.Index + @context.Name + @context.Interval + + + + + + + +
+
+
+ + + +
Mã Script đầy đủ
+ +
@_selectedCode
+
+
+ + Đóng + +
+ +@code { + [CascadingParameter] + private ProcessorHubClient ProcessorHub { get; set; } = null!; + + private string TableHeight = "0px"; + private ElementReference divRef; + private ElementReference toolbarRef; + + private HttpClient http = default!; + private List scriptTasks = []; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + var rect = await divRef.MudGetBoundingClientRectAsync(); + var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync(); + TableHeight = $"{rect.Height - 25 - Math.Max(toolbarRect.Height, 64)}px"; + StateHasChanged(); + } + } + + // Dialog state + private bool _codeDialogOpen = false; + private string? _selectedCode; + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + http = httpFactory.CreateClient("ScriptManagerAPI"); + await ReloadData(); + + if (ProcessorHub.IsConnected) + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + } + + ProcessorHub.StateChanged += OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged += OnConnectionStateChanged; + + ScriptTaskHub.TaskResumed += OnTaskResumed; + ScriptTaskHub.TaskPaused += OnTaskPaused; + + await ScriptTaskHub.StartAsync(); + } + + private void OnConnectionStateChanged(HubConnectionState state) + { + if (state == HubConnectionState.Connected) + { + _ = InvokeAsync(async Task () => + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + }); + + } + } + + private bool isRebuild = false; + private bool actionDisable = true; + private void OnProcessorStateChanged(ProcessorState state) + { + if (state == ProcessorState.Building) + { + isRebuild = true; + } + else if (state == ProcessorState.Ready) + { + if (isRebuild) + { + isRebuild = false; + _ = InvokeAsync(async Task () => + { + await ReloadData(); + }); + } + } + + if (actionDisable) + { + if (state == ProcessorState.Running) + { + actionDisable = false; + StateHasChanged(); + } + } + else + { + if (state != ProcessorState.Running) + { + actionDisable = true; + StateHasChanged(); + } + } + + if (state == ProcessorState.Running) + { + _ = InvokeAsync(async Task () => + { + var states = await ScriptTaskHub.GetTaskStates(); + foreach (var task in scriptTasks) + { + task.Enable = states.ContainsKey(task.Name) && states[task.Name]; + } + StateHasChanged(); + }); + } + } + + private async Task ReloadData() + { + await InvokeAsync(async Task () => + { + scriptTasks.Clear(); + var tasks = await http.GetFromJsonAsync>("/api/ScriptTasks") ?? []; + foreach (var item in tasks.Select((value, index) => (value, index))) + { + scriptTasks.Add(new ScriptTaskModel(item.index + 1, item.value)); + } + if (!actionDisable) + { + var states = await ScriptTaskHub.GetTaskStates(); + foreach (var task in scriptTasks) + { + task.Enable = states.ContainsKey(task.Name) && states[task.Name]; + } + + } + StateHasChanged(); + }); + } + + private void ShowCodeDialog(string code) + { + _selectedCode = code; + _codeDialogOpen = true; + } + + private void CloseCodeDialog() + { + _codeDialogOpen = false; + _selectedCode = null; + } + + private void OnTaskResumed(string taskName) + { + foreach (var task in scriptTasks) + { + if (task.Name == taskName) + { + task.Enable = true; + StateHasChanged(); + break; + } + } + } + + private void OnTaskPaused(string taskName) + { + foreach (var task in scriptTasks) + { + if (task.Name == taskName) + { + task.Enable = false; + StateHasChanged(); + break; + } + } + } + + private async Task RequestTaskAction(string name, bool enable) + { + if (actionDisable) return; + if (enable) + { + var result = await ScriptTaskHub.Resume(name); + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + OnTaskPaused(name); + } + } + else + { + var result = await ScriptTaskHub.Pause(name); + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + OnTaskResumed(name); + } + } + } + + public async ValueTask DisposeAsync() + { + ProcessorHub.StateChanged -= OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged -= OnConnectionStateChanged; + ScriptTaskHub.TaskResumed -= OnTaskResumed; + ScriptTaskHub.TaskPaused -= OnTaskPaused; + await ScriptTaskHub.StartAsync(); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dashboards/Variables.razor b/RobotNet.WebApp/Scripts/Components/Dashboards/Variables.razor new file mode 100644 index 0000000..d4c62fd --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dashboards/Variables.razor @@ -0,0 +1,185 @@ +@implements IDisposable + +@using Microsoft.AspNetCore.SignalR.Client +@using RobotNet.Clients +@using RobotNet.Shares + +@inject IHttpClientFactory httpFactory +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +
+
+ + +
+

Global Variables

+ + +
+
+ + + + + + + + + + Name + Type + Value + + + + @context.Index + @context.Name + @context.TypeName + @context.Value + + + + + + + + + +
+
+
+ +@code { + [CascadingParameter] + private ProcessorHubClient ProcessorHub { get; set; } = null!; + + private string TableHeight = "0px"; + private ElementReference divRef; + private ElementReference toolbarRef; + + private HttpClient http = default!; + private List _items = []; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + var rect = await divRef.MudGetBoundingClientRectAsync(); + var toolbarRect = await toolbarRef.MudGetBoundingClientRectAsync(); + TableHeight = $"{rect.Height - 25 - Math.Max(toolbarRect.Height, 64)}px"; + StateHasChanged(); + } + } + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + http = httpFactory.CreateClient("ScriptManagerAPI"); + await ReloadData(); + + if (ProcessorHub.IsConnected) + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + } + + ProcessorHub.StateChanged += OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged += OnConnectionStateChanged; + } + + + private void OnConnectionStateChanged(HubConnectionState state) + { + if (state == HubConnectionState.Connected) + { + _ = InvokeAsync(async Task () => + { + OnProcessorStateChanged(await ProcessorHub.GetState()); + }); + + } + } + + private bool CallDisabled = true; + private void OnProcessorStateChanged(ProcessorState state) + { + if (state == ProcessorState.Ready) + { + _ = InvokeAsync(async Task () => + { + await ReloadData(); + }); + } + + if (CallDisabled) + { + if (state == ProcessorState.Running) + { + CallDisabled = false; + StateHasChanged(); + } + } + else + { + if (state != ProcessorState.Running) + { + CallDisabled = true; + StateHasChanged(); + } + } + } + + private async Task ReloadData() + { + _items.Clear(); + + var vars = await http.GetFromJsonAsync>("/api/ScriptVariables") ?? []; + foreach (var item in vars.Select((value, index) => (value, index))) + { + _items.Add(new ScriptVariableModel(item.index + 1, item.value)); + } + StateHasChanged(); + } + + private async Task ResetValue(ScriptVariableModel model) + { + if (CallDisabled) return; + + var result = await http.PutFromJsonAsync($"/api/ScriptVariables/{model.Name}/Reset", new UpdateVariableModel(model.Name, "")); + if(result == null) + { + Snackbar.Add("Failed to reset variable value: server response null", Severity.Error); + } + else if (result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Success); + } + else + { + Snackbar.Add($"Failed to reset variable value: {result.Message}", Severity.Error); + } + } + + private async Task ShowSetValueDialog(ScriptVariableModel model) + { + + if (CallDisabled) return; + + var parameters = new DialogParameters() + { + {x => x.Model, model}, + }; + + var dialog = await DialogService.ShowAsync("Set Variable Value", parameters); + var resultDialog = await dialog.Result; + } + + public void Dispose() + { + ProcessorHub.StateChanged -= OnProcessorStateChanged; + ProcessorHub.ConnectionStateChanged -= OnConnectionStateChanged; + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dialogs/CreateNewFileOrFolderDialog.razor b/RobotNet.WebApp/Scripts/Components/Dialogs/CreateNewFileOrFolderDialog.razor new file mode 100644 index 0000000..c5993b9 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dialogs/CreateNewFileOrFolderDialog.razor @@ -0,0 +1,64 @@ +@inject ISnackbar Snackbar + + + + + @Title + + + +
+ +
+
+ + Cancel + Create + +
+ +@code { + [CascadingParameter] + private IMudDialogInstance Dialog { get; set; } = null!; + + [Parameter] + public string FolderName { get; set; } = ""; + + [Parameter] + public bool IsFolder { get; set; } = false; + + private string Title => $"Tạo mới {(IsFolder ? "folder" : "file code")}"; + private string Label => $"/{FolderName}"; + private Adornment AdornmentVisible => IsFolder ? Adornment.None : Adornment.End; + + private string FileName { get; set; } = ""; + + private string NameErrorText = ""; + private bool NameError = false; + + private bool CreateButtonDisable => NameError; + + private void OnCancel() => Dialog.Cancel(); + + public void OnCreate() + { + ValidateName(); + + if (NameError) + { + Snackbar.Add("Tên chưa đúng!", Severity.Error); + } + else + { + Dialog.Close(DialogResult.Ok(FileName)); + } + } + + private void ValidateName() + { + NameErrorText = string.IsNullOrEmpty(FileName) ? "Tên không được để trống" : ""; + NameError = !string.IsNullOrEmpty(NameErrorText); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dialogs/DeleteFileOrFolderDialog.razor b/RobotNet.WebApp/Scripts/Components/Dialogs/DeleteFileOrFolderDialog.razor new file mode 100644 index 0000000..50d06de --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dialogs/DeleteFileOrFolderDialog.razor @@ -0,0 +1,98 @@ +@inject ISnackbar Snackbar + + + + + Danh sách script sẽ xóa + + + +
+ @foreach (var file in DeleteList) + { + + @file + + } +
+
+ + Cancel + Delete + +
+ +@code { + [CascadingParameter] + private IMudDialogInstance Dialog { get; set; } = null!; + + [Parameter] + public ScriptWorkspace Workspace { get; set; } = null!; + + private IEnumerable DeleteList = []; + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (Workspace.SelectedFile is not null) + { + DeleteList = [Workspace.SelectedFile.Path]; + StateHasChanged(); + } + else if (Workspace.SelectedFolder is not null) + { + DeleteList = GetListFromFolder(Workspace.SelectedFolder); + StateHasChanged(); + } + } + + + private IEnumerable GetListFromFolder(ScriptFolderModel folder) + { + var list = new List(); + list.Add(folder.Path); + + foreach (var dir in folder.Folders) + { + list.AddRange(GetListFromFolder(dir)); + } + + foreach (var file in folder.Files) + { + list.Add(file.Path); + } + + return list; + } + + private void OnCancel() => Dialog.Cancel(); + + private async Task OnDelete() + { + if (Workspace.SelectedFile is not null) + { + var result = await Workspace.DeleteFile(Workspace.SelectedFile); + if (result.IsSuccess) + { + Dialog.Close(); + } + else + { + Snackbar.Add(result.Message, Severity.Error); + } + } + else if (Workspace.SelectedFolder is not null) + { + var result = await Workspace.DeleteFolder(Workspace.SelectedFolder); + if (result.IsSuccess) + { + Dialog.Close(); + } + else + { + Snackbar.Add(result.Message, Severity.Error); + } + } + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Dialogs/RenameFileOrFolderDialog.razor b/RobotNet.WebApp/Scripts/Components/Dialogs/RenameFileOrFolderDialog.razor new file mode 100644 index 0000000..bfa592f --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Dialogs/RenameFileOrFolderDialog.razor @@ -0,0 +1,107 @@ +@inject ISnackbar Snackbar + + + + + @Title + + + +
+ +
+
+ + Cancel + Rename + +
+ +@code { + + [CascadingParameter] + private IMudDialogInstance Dialog { get; set; } = null!; + + [Parameter] + public ScriptWorkspace Workspace { get; set; } = null!; + + private bool IsFolder; + private string Label = ""; + private string Title => IsFolder ? "Rename folder" : "Rename file"; + private string NameErrorText = ""; + private bool NameError = false; + private string FileName = ""; + private string OldName = ""; + private bool RenameDisabled => FileName == OldName; + private Adornment AdornmentVisible => IsFolder ? Adornment.None : Adornment.End; + + protected override void OnInitialized() + { + base.OnInitialized(); + if (Workspace.SelectedFile is not null) + { + IsFolder = false; + Label = Workspace.SelectedFile.Parrent?.Name ?? "/"; + FileName = Workspace.SelectedFile.Name; + if (FileName.EndsWith(".cs")) + { + FileName = FileName.Substring(0, FileName.Length - 3); + } + OldName = FileName; + } + else if (Workspace.SelectedFolder is not null) + { + IsFolder = true; + Label = Workspace.SelectedFolder.Parrent?.Name ?? "/"; + FileName = Workspace.SelectedFolder.Name; + OldName = FileName; + } + } + + private void ValidateName() + { + NameErrorText = string.IsNullOrEmpty(FileName) ? "Tên không được để trống" : ""; + NameError = !string.IsNullOrEmpty(NameErrorText); + } + + private void OnCancel() => Dialog.Cancel(); + + private async Task OnRename() + { + ValidateName(); + + if (NameError) + { + Snackbar.Add("Tên chưa đúng!", Severity.Error); + } + else + { + if (Workspace.SelectedFile is not null) + { + var result = await Workspace.Rename(Workspace.SelectedFile, FileName.EndsWith(".cs") ? FileName : FileName + ".cs"); + if (result.IsSuccess) + { + Dialog.Close(); + } + else + { + Snackbar.Add(result.Message, Severity.Error); + } + } + else if (Workspace.SelectedFolder is not null) + { + var result = await Workspace.Rename(Workspace.SelectedFolder, FileName); + if (result.IsSuccess) + { + Dialog.Close(); + } + else + { + Snackbar.Add(result.Message, Severity.Error); + } + } + } + } +} diff --git a/RobotNet.WebApp/Scripts/Components/Editor.razor b/RobotNet.WebApp/Scripts/Components/Editor.razor new file mode 100644 index 0000000..9e51780 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Editor.razor @@ -0,0 +1,262 @@ +@implements IAsyncDisposable + +@using Microsoft.AspNetCore.Components +@using Microsoft.CodeAnalysis +@using Microsoft.CodeAnalysis.Text +@using Microsoft.JSInterop +@using RobotNet.WebApp.Scripts.Monaco +@using RobotNet.WebApp.Scripts.Monaco.Editor +@using RobotNet.WebApp.Scripts.Monaco.Languages +@using System.Timers + +@inject IJSRuntime JsRuntime +@inject ISnackbar Snackbar + +
+
+
+
@NameFile
+
+
+
+
+ + +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + private object dotNetHelper = default!; + private IJSInProcessObjectReference? editor; + private IJSInProcessObjectReference? editorModel; + private IJSInProcessObjectReference? disposableCompletionItemProvider; + private IJSInProcessObjectReference? disposableSignatureHelpProvider; + private IJSInProcessObjectReference? disposableHoverProvider; + + private bool IsEditorInitialized; + private string Code = ""; + private string NameFile = ""; + public ProcessorState State { get; private set; } = ProcessorState.Idle; + private bool IsReadOnly = true; + + private ElementReference EditorContainer; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + if (firstRender) + { + dotNetHelper = DotNetObjectReference.Create(this); + + editor = await JsRuntime.InvokeAsync("monaco.editor.create", EditorContainer, new + { + Value = "", + Language = "csharp", + Theme = "vs-dark", + GlyphMargin = true, + AutomaticLayout = true, + ReadOnly = IsReadOnly, + }); + + editorModel = await editor.InvokeAsync("getModel"); + + await JsRuntime.InvokeVoidAsync("MonacoRuntime.OnCodeEditorDidChangeModelContent", editor, dotNetHelper, nameof(CodeEditorDidChangeModelContent)); + disposableCompletionItemProvider = await JsRuntime.InvokeAsync("MonacoRuntime.CSharpLanguageRegisterCompletionItemProvider", dotNetHelper, nameof(GetCompletionItems)); + disposableSignatureHelpProvider = await JsRuntime.InvokeAsync("MonacoRuntime.CSharpLanguageRegisterSignatureHelpProvider", dotNetHelper, nameof(GetSignatureHelp)); + disposableHoverProvider = await JsRuntime.InvokeAsync("MonacoRuntime.CSharpLanguageRegisterHoverProvider", dotNetHelper, nameof(GetQuickInfo)); + IsEditorInitialized = true; + + Workspace.OnFileChanged += FileChanged; + Workspace.OnDiagnoticChanged += DiagnoticChanged; + } + } + + public void OnProcessStateChanged(ProcessorState state) + { + State = state; + var isReadOnly = State != ProcessorState.Ready && State != ProcessorState.Idle && State != ProcessorState.BuildError; + if (isReadOnly != IsReadOnly) + { + IsReadOnly = isReadOnly; + if (editor != null) + { + _ = InvokeAsync(async Task () => + { + await editor.InvokeVoidAsync("updateOptions", new + { + ReadOnly = IsReadOnly, + }); + }); + } + } + } + + private async Task FileChanged(ScriptFileModel? file) + { + await this.InvokeAsync(async Task () => + { + if (file is null) + { + NameFile = ""; + Code = ""; + } + else + { + NameFile = file.Name; + Code = file.EditCode; + } + if (IsEditorInitialized && editor is not null) + { + await editor.InvokeVoidAsync("setValue", Code); + await JsRuntime.InvokeVoidAsync("monaco.editor.setModelMarkers", editorModel, "default", file?.Diagnostics.Select(ToMonacoDiagnostic) ?? []); + // await editor.InvokeVoidAsync("updateOptions", new + // { + // ReadOnly = IsReadOnly, + // }); + } + StateHasChanged(); + }); + } + + private async Task DiagnoticChanged(IEnumerable diagnostics) + { + await this.InvokeAsync(async Task () => + { + if (IsEditorInitialized && editor is not null) + { + await JsRuntime.InvokeVoidAsync("monaco.editor.setModelMarkers", editorModel, "default", diagnostics.Select(ToMonacoDiagnostic) ?? []); + } + }); + + } + + [JSInvokable] + public async Task GetCompletionItems(int line, int column, int kind, char? triggerCharacter) + { + return new() + { + Suggestions = [.. await Workspace.GetCompletionCurrentFileAsync(line, column, kind, triggerCharacter)], + }; + } + + [JSInvokable] + public async Task GetQuickInfo(int line, int column) + { + return await Workspace.GetQuickInfoCurrentFile(line, column); + } + + [JSInvokable] + public async Task GetSignatureHelp(int line, int column) + { + return await Workspace.GetSignatureHelpCurrentFile(line, column); + } + + [JSInvokable] + public async Task CodeEditorDidChangeModelContent(ModelContentChangedEvent e) + { + if (e.IsFlush) return; + + if (e.Changes.Length != 0) + { + if (editor is not null) + { + var code = await editor.InvokeAsync("getValue"); + + if (code != Code) + { + Code = code; + Workspace.WriteDocument(Code); + } + + } + } + } + + bool isSaving = false; + private async Task OnKeyPress(KeyboardEventArgs e) + { + if ((e.Key == "s" || e.Key == "S") && e.CtrlKey) + { + if(IsReadOnly) + { + Snackbar.Add($"Hệ thống đang ở trạng thái {State}, Không thể thay đổi script", Severity.Warning); + return; + } + if (isSaving) return; + isSaving = true; + var result = await Workspace.SaveCurrentFileAsync(); + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + } + isSaving = false; + } + } + + public async ValueTask DisposeAsync() + { + if (disposableCompletionItemProvider != null) + { + await disposableCompletionItemProvider.InvokeVoidAsync("dispose"); + await disposableCompletionItemProvider.DisposeAsync(); + disposableCompletionItemProvider = null; + } + + if (disposableSignatureHelpProvider != null) + { + await disposableSignatureHelpProvider.InvokeVoidAsync("dispose"); + await disposableSignatureHelpProvider.DisposeAsync(); + disposableSignatureHelpProvider = null; + } + + if (disposableHoverProvider != null) + { + await disposableHoverProvider.InvokeVoidAsync("dispose"); + await disposableHoverProvider.DisposeAsync(); + disposableHoverProvider = null; + } + + if (editorModel != null) + { + await editorModel.InvokeVoidAsync("dispose"); + await editorModel.DisposeAsync(); + editorModel = null; + } + + if (editor != null) + { + await editor.InvokeVoidAsync("dispose"); + await editor.DisposeAsync(); + editor = null; + } + + if (IsEditorInitialized) + { + Workspace.OnFileChanged -= FileChanged; + Workspace.OnDiagnoticChanged -= DiagnoticChanged; + } + + GC.SuppressFinalize(this); + } + + + private static MarkerData ToMonacoDiagnostic(Diagnostic diagnostic) + { + var lineSpan = diagnostic.Location.GetLineSpan(); + return new() + { + StartLineNumber = lineSpan.StartLinePosition.Line + 1, + StartColumn = lineSpan.StartLinePosition.Character + 1, + EndLineNumber = lineSpan.EndLinePosition.Line + 1, + EndColumn = lineSpan.EndLinePosition.Character + 1, + Message = diagnostic.GetMessage(), + Severity = diagnostic.Severity switch + { + DiagnosticSeverity.Info => MarkerSeverity.Info, + DiagnosticSeverity.Warning => MarkerSeverity.Warning, + DiagnosticSeverity.Error => MarkerSeverity.Error, + _ => MarkerSeverity.Hint, + }, + }; + } +} diff --git a/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor b/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor new file mode 100644 index 0000000..8a0cf47 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor @@ -0,0 +1,113 @@ +@using RobotNet.WebApp.Scripts.Components.Dialogs +@implements IDisposable + +@inject IJSRuntime JsRuntime +@inject ISnackbar Snackbar +@inject IDialogService DialogService + + + +
+
+ + @foreach (var model in Workspace.Folders) + { + + } + @foreach (var model in Workspace.Files) + { + + } + +
+
+
+ + + + + + +
+ +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + readonly string ItemRadioName = $"script-{Guid.NewGuid()}"; + + protected override void OnInitialized() + { + base.OnInitialized(); + Workspace.RootChanged += OnRootChanged; + } + + private void OnRootChanged() + { + this.InvokeAsync(() => + { + StateHasChanged(); + }); + } + + private async Task ResetSelect() + { + Workspace.SelectFolder(null); + await JsRuntime.InvokeVoidAsync("UncheckRadioAll", ItemRadioName); + } + + private async Task SaveSelect() + { + if (Workspace.IsReadOnly) + { + Snackbar.Add($"Hệ thống đang ở trạng thái {Workspace.ProcessorState}, Không thể thay đổi script", Severity.Warning); + return; + } + var results = await Workspace.SaveSelectAsync(); + foreach (var result in results) + { + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + } + } + } + + private async Task DeleteSelect() + { + if (Workspace.IsReadOnly) + { + Snackbar.Add($"Hệ thống đang ở trạng thái {Workspace.ProcessorState}, Không thể thay đổi script", Severity.Warning); + return; + } + // Truyền Workspace vào DeleteFileOrFolderDialog qua parameters + var parameters = new DialogParameters + { + { "Workspace", Workspace } + }; + + await DialogService.ShowAsync("", parameters: parameters); + } + + private async Task RenameSelect() + { + if (Workspace.IsReadOnly) + { + Snackbar.Add($"Hệ thống đang ở trạng thái {Workspace.ProcessorState}, Không thể thay đổi script", Severity.Warning); + return; + } + var parameters = new DialogParameters + { + { "Workspace", Workspace } + }; + + await DialogService.ShowAsync("", parameters: parameters); + } + + public void Dispose() + { + Workspace.RootChanged -= OnRootChanged; + GC.SuppressFinalize(this); + } +} + diff --git a/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor.css b/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor.css new file mode 100644 index 0000000..44653a0 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/EditorHierachy.razor.css @@ -0,0 +1,20 @@ +.editor-hierachy { + width: 100%; + flex-grow: 1; + position: relative; + overflow: hidden; + background-color: rgb(30, 30, 30); +} + + .editor-hierachy > div { + width: 100%; + height: 100%; + position: absolute; + top: 0px; + left: 0px; + overflow: hidden; + } + + .editor-hierachy > div:hover { + overflow: auto; + } diff --git a/RobotNet.WebApp/Scripts/Components/FileItem.razor b/RobotNet.WebApp/Scripts/Components/FileItem.razor new file mode 100644 index 0000000..28c0692 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/FileItem.razor @@ -0,0 +1,70 @@ +@implements IDisposable + +@inject IJSRuntime JsRuntime + +
+ + @for (int i = 0; i < Model.Level; i++) + { +
+ } +
+ +
+ + + +
+ +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + [CascadingParameter(Name = "HierachyItemRadioName")] + protected string RadioName { get; set; } = "script-explorer-item"; + + [Parameter, EditorRequired] + public ScriptFileModel Model { get; set; } = default!; + + Guid ItemId = Guid.NewGuid(); + + private string Icon => "mdi-language-csharp"; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (!firstRender) return; + + Model.OnModified += OnModified; + Model.OnDiagnosticsChanged += OnDiagnosticsChanged; + } + + private void OnModified() + { + this.InvokeAsync(() => + { + StateHasChanged(); + }); + } + + private void OnDiagnosticsChanged(int warningCount, int errorCount) + { + this.InvokeAsync(() => + { + StateHasChanged(); + }); + } + + private async Task Click() + { + await JsRuntime.InvokeVoidAsync("SetRadioChecked", ItemId); + await Workspace.SelectFile(this.Model); + } + + public void Dispose() + { + Model.OnModified -= OnModified; + Model.OnDiagnosticsChanged -= OnDiagnosticsChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/FileItem.razor.css b/RobotNet.WebApp/Scripts/Components/FileItem.razor.css new file mode 100644 index 0000000..56c8798 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/FileItem.razor.css @@ -0,0 +1,44 @@ +.file-item { + width: 100%; + height: 22px; + display: flex; + flex-direction: row; + align-items: center; +} + + .file-item:hover { + background-color: rgba(0,0,0,0.3); + } + + .file-item:active { + background-color: transparent !important; + } + + .file-item:has(input:checked) { + background-color: rgb(55, 55, 61); + } + + .file-item:has(input:checked):focus { + background-color: rgb(4, 57, 94); + outline-color: rgb(0, 120, 212); + } + +.indent-guide { + border-right: 1px solid rgba(88,88,88,0.4); + width: 8px; + display: inline; + height: 100%; +} + +.file-icon { + display: flex; + height: 16px; + width: 16px; + font-size: 16px; + overflow: hidden; + align-items: center; + justify-content: center; + margin-right: 6px; + margin-left: 6px; + color: #519aba; +} diff --git a/RobotNet.WebApp/Scripts/Components/FolderItem.razor b/RobotNet.WebApp/Scripts/Components/FolderItem.razor new file mode 100644 index 0000000..9011b15 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/FolderItem.razor @@ -0,0 +1,96 @@ +@implements IDisposable + +@inject IJSRuntime JsRuntime + +
+ + @for (int i = 0; i < Model.Level; i++) + { +
+ } +
+ +
+ + + +
+ +@if (IsExpaned) +{ + @foreach (var model in Model.Folders) + { + + } + @foreach (var model in Model.Files) + { + + } +} + +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + [CascadingParameter(Name = "HierachyItemRadioName")] + protected string RadioName { get; set; } = "script-explorer-item"; + + [Parameter, EditorRequired] + public ScriptFolderModel Model { get; set; } = default!; + + Guid ItemId = Guid.NewGuid(); + + public bool IsExpaned { get; private set; } + private string Icon => IsExpaned ? "mdi-chevron-down" : "mdi-chevron-right"; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + Model.OnChildrenChanged += OnChildrenChanged; + } + } + + private void OnChildrenChanged() + { + this.InvokeAsync(() => + { + StateHasChanged(); + }); + } + + private async Task Click() + { + await JsRuntime.InvokeVoidAsync("SetRadioChecked", ItemId); + + if (Workspace.SelectFolder(this.Model)) return; + + if (IsExpaned) + { + Collapse(); + } + else + { + Expand(); + } + } + + public void Collapse() + { + IsExpaned = false; + StateHasChanged(); + } + + public void Expand() + { + IsExpaned = true; + StateHasChanged(); + } + + public void Dispose() + { + Model.OnChildrenChanged -= OnChildrenChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/FolderItem.razor.css b/RobotNet.WebApp/Scripts/Components/FolderItem.razor.css new file mode 100644 index 0000000..b0db448 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/FolderItem.razor.css @@ -0,0 +1,47 @@ +.folder-item { + width: 100%; + height: 22px; + display: flex; + flex-direction: row; + align-items: center; +} + + .folder-item:hover { + background-color: rgba(0,0,0,0.3); + } + + .folder-item:active { + background-color: transparent !important; + } + + .folder-item:has(input:checked) { + background-color: rgb(55, 55, 61); + } + + .folder-item:has(input:checked):focus { + background-color: rgb(4, 57, 94); + outline-color: rgb(0, 120, 212); + } + +.indent-guide { + border-right: 1px solid rgba(88,88,88,0.4); + width: 8px; + display: inline; + height: 100%; +} + +.folder-icon { + display: flex; + height: 16px; + width: 16px; + font-size: 16px; + overflow: hidden; + align-items: center; + justify-content: center; + margin-right: 6px; + margin-left: 6px; +} + + .folder-icon .mdi-language-csharp { + color: #519aba; + } diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor b/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor new file mode 100644 index 0000000..24cb6c2 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor @@ -0,0 +1,44 @@ +@implements IDisposable + +@if (Model.ErrorCount > 0) +{ +
+ @ErrorStr +
+} +@if (Model.WarningCount > 0) +{ +
+ @WarningStr +
+} + +@code { + + [Parameter, EditorRequired] + public IHierachyItemModel Model { get; set; } = default!; + + private string ErrorStr => Model.ErrorCount < 10 ? Model.ErrorCount.ToString() : "9+"; + + private string WarningStr => Model.WarningCount < 10 ? Model.WarningCount.ToString() : "9+"; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + Model.OnDiagnosticsChanged += OnDiagnosticsChanged; + } + } + + private void OnDiagnosticsChanged(int warningCount, int errorCount) + { + StateHasChanged(); + } + + public void Dispose() + { + Model.OnDiagnosticsChanged -= OnDiagnosticsChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor.css b/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor.css new file mode 100644 index 0000000..eab693a --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemDiagnostics.razor.css @@ -0,0 +1,29 @@ +.folder-error { + width: 18px; + height: 18px; + color: white; + font-size: 11px; + margin-right: 3px; + border-radius: 50%; + background-color: firebrick; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + overflow: hidden; +} + +.folder-warnning { + width: 18px; + height: 18px; + color: white; + font-size: 11px; + margin-right: 3px; + border-radius: 50%; + background-color: darkgoldenrod; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + overflow: hidden; +} diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor b/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor new file mode 100644 index 0000000..27f4591 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor @@ -0,0 +1,28 @@ +@if (Model.IsModified) +{ +
+ M +
+} + +@code { + + [Parameter, EditorRequired] + public IHierachyItemModel Model { get; set; } = default!; + + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + Model.OnModified += StateHasChanged; + } + } + + public void Dispose() + { + Model.OnModified -= StateHasChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor.css b/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor.css new file mode 100644 index 0000000..6af9e73 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemModified.razor.css @@ -0,0 +1,14 @@ +.folder-modified { + width: 18px; + height: 18px; + color: white; + font-size: 11px; + margin-right: 3px; + border-radius: 3px; + background-color: teal; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + overflow: hidden; +} diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor b/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor new file mode 100644 index 0000000..96925f6 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor @@ -0,0 +1,24 @@ +
+ @Model.Name +
+ +@code { + + [Parameter, EditorRequired] + public IHierachyItemModel Model { get; set; } = default!; + + protected override void OnAfterRender(bool firstRender) + { + base.OnAfterRender(firstRender); + if (firstRender) + { + Model.OnNameChanged += StateHasChanged; + } + } + + public void Dispose() + { + Model.OnNameChanged -= StateHasChanged; + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor.css b/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor.css new file mode 100644 index 0000000..2e7021b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/HierachyItemName.razor.css @@ -0,0 +1,6 @@ +.folder-label { + color: rgb(204,204,204); + flex-grow: 1; + font-size: 13px; + user-select: none; +} diff --git a/RobotNet.WebApp/Scripts/Components/Loading.razor b/RobotNet.WebApp/Scripts/Components/Loading.razor new file mode 100644 index 0000000..1db8195 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/Loading.razor @@ -0,0 +1,7 @@ +
+ +
+ +@code { + +} diff --git a/RobotNet.WebApp/Scripts/Components/ProcessorConsole.razor b/RobotNet.WebApp/Scripts/Components/ProcessorConsole.razor new file mode 100644 index 0000000..e572d3b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ProcessorConsole.razor @@ -0,0 +1,70 @@ +@using Microsoft.CodeAnalysis + +@implements IAsyncDisposable + +@inject ConsoleHubClient consoleHub +@inject IJSRuntime JS + +
+
+
+ @foreach(var console in Consoles) + { + + } +
+
+
+ +@code { + private ElementReference ScrollViewDiv; + private List Consoles = []; + + public void AddInfo(string message) => AddMessage(DiagnosticSeverity.Info, message); + public void AddWarning(string message) => AddMessage(DiagnosticSeverity.Warning, message); + public void AddError(string message) => AddMessage(DiagnosticSeverity.Error, message); + + public async void AddMessage(DiagnosticSeverity severity, string message) + { + if (Consoles.Any() && Consoles[^1].Severity == severity) + { + Consoles[^1].Append(message); + } + else + { + Consoles.Add(new ConsoleItemModel(severity, message)); + StateHasChanged(); + } + + // Đợi UI cập nhật xong rồi scroll + await Task.Yield(); + await JS.InvokeVoidAsync("ScrollToBottom", ScrollViewDiv); + } + + public void Clear() + { + Consoles.Clear(); + StateHasChanged(); + } + + + protected override async Task OnInitializedAsync() + { + await base.OnInitializedAsync(); + + consoleHub.MessageInfo += AddInfo; + consoleHub.MessageWarning += AddWarning; + consoleHub.MessageError += AddError; + await consoleHub.StartAsync(); + await consoleHub.RegisterAllConsoles(); + } + + public async ValueTask DisposeAsync() + { + consoleHub.MessageInfo -= AddInfo; + consoleHub.MessageWarning -= AddWarning; + consoleHub.MessageError -= AddError; + await consoleHub.UnregisterAllConsoles(); + await consoleHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/ProcessorControl.razor b/RobotNet.WebApp/Scripts/Components/ProcessorControl.razor new file mode 100644 index 0000000..6080155 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/ProcessorControl.razor @@ -0,0 +1,172 @@ +@implements IAsyncDisposable + +@using Microsoft.CodeAnalysis + +@inject ProcessorHubClient processorHub +@inject ISnackbar Snackbar +@inject IDialogService DialogService + +
+
+
+ + + + + +
+
+ +
+
+ +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + private ProcessorConsole? processorConsole; + public ProcessorState State { get; private set; } = ProcessorState.Idle; + private ProcessorRequest Request = ProcessorRequest.None; + + private bool ResetDisable => State != ProcessorState.Idle && State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError; + private bool BuildDisable => State != ProcessorState.Idle && State != ProcessorState.Ready && State != ProcessorState.Error && State != ProcessorState.BuildError; + private bool RunDisable => State != ProcessorState.Ready; + private bool StopDisable => State != ProcessorState.Running; + + protected override async Task OnInitializedAsync() + { + processorHub.StateChanged += OnStateChanged; + processorHub.RequestChanged += OnRequestChanged; + await processorHub.StartAsync(); + + OnStateChanged(await processorHub.GetState()); + } + + private void OnStateChanged(ProcessorState state) + { + if (State == state) return; + + State = state; + StateHasChanged(); + } + + private void OnRequestChanged(ProcessorRequest request) + { + Request = request; + StateHasChanged(); + } + + private async Task Reset() + { + var result = await processorHub.Reset(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to reset processor: {result.Message}", Severity.Error); + } + } + + private async Task Build() + { + if (Workspace.AnyChanges()) + { + bool? confirm = await DialogService.ShowMessageBox("Cảnh báo", "Bạn có muốn lưu lại tất cả thay đổi trước khi build không?", yesText: "Save All", cancelText: "Cancel"); + + if (confirm != true) return; + + bool isError = false; + var saveResults = await Workspace.SaveAllAsync(); + foreach (var saveResult in saveResults) + { + if (!saveResult.IsSuccess) + { + if (processorConsole != null) + { + processorConsole.AddError(saveResult.Message); + } + else + { + Snackbar.Add(saveResult.Message, Severity.Error); + } + isError = true; + } + } + + if (isError) + { + return; + } + else + { + await Task.Delay(500); + } + } + + var diagnostics = await Workspace.GetAllDiagnostics(); + if (diagnostics.Any(d => d.Severity == DiagnosticSeverity.Warning || d.Severity == DiagnosticSeverity.Error)) + { + foreach (var diagnostic in diagnostics) + { + if (diagnostic.Severity == DiagnosticSeverity.Warning) + { + if (processorConsole != null) + { + processorConsole.AddWarning($"File /{diagnostic}"); + } + else + { + Snackbar.Add($"File /{diagnostic}", Severity.Warning); + } + } + else if (diagnostic.Severity == DiagnosticSeverity.Error) + { + if (processorConsole != null) + { + processorConsole.AddError($"File /{diagnostic}"); + } + else + { + Snackbar.Add($"File /{diagnostic}", Severity.Error); + } + } + } + } + else + { + var result = await processorHub.Build(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to build processor: {result.Message}", Severity.Error); + } + } + } + + private async Task Run() + { + var result = await processorHub.Run(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to run processor: {result.Message}", Severity.Error); + } + } + + private async Task Stop() + { + var result = await processorHub.Stop(); + if (!result.IsSuccess) + { + Snackbar.Add($"Failed to stop processor: {result.Message}", Severity.Error); + } + } + + private void ClearConsole() + { + processorConsole?.Clear(); + } + + public async ValueTask DisposeAsync() + { + processorHub.StateChanged -= OnStateChanged; + processorHub.RequestChanged -= OnRequestChanged; + await processorHub.StopAsync(); + } +} diff --git a/RobotNet.WebApp/Scripts/Components/SidebarActions.razor b/RobotNet.WebApp/Scripts/Components/SidebarActions.razor new file mode 100644 index 0000000..501b13b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/SidebarActions.razor @@ -0,0 +1,75 @@ +@using RobotNet.WebApp.Scripts.Components.Dialogs + +@inject IDialogService DialogService +@inject ISnackbar Snackbar + + + +@code { + [CascadingParameter] + private ScriptWorkspace Workspace { get; set; } = null!; + + private async Task OpenDialogCreateNewFile() + { + var parentPath = Workspace.SelectedFolder?.Path ?? ""; + + var parameters = new DialogParameters() + { + {x => x.FolderName, parentPath}, + }; + + var dialog = await DialogService.ShowAsync("Create new file", parameters); + var resultDialog = await dialog.Result; + + if (resultDialog is null || resultDialog.Canceled || resultDialog.Data is not string fileName) return; + + fileName = fileName.EndsWith(".cs") ? fileName : fileName + ".cs"; + var result = await Workspace.CreateNewFile(fileName); + + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + } + } + private async Task OpenDialogCreateNewFolder() + { + + var parentPath = Workspace.SelectedFolder?.Path ?? ""; + var parameters = new DialogParameters() + { + {x => x.FolderName, parentPath}, + {x => x.IsFolder, true }, + }; + + var dialog = await DialogService.ShowAsync("Create new folder", parameters); + var resultDialog = await dialog.Result; + + if (resultDialog is null || resultDialog.Canceled || resultDialog.Data is not string folderName) return; + + var result = await Workspace.CreateNewFolder(folderName); + + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + } + } + + private async Task SaveAllFiles() + { + var results = await Workspace.SaveAllAsync(); + foreach(var result in results) + { + if (!result.IsSuccess) + { + Snackbar.Add(result.Message, Severity.Error); + } + } + } +} diff --git a/RobotNet.WebApp/Scripts/Components/SidebarActions.razor.css b/RobotNet.WebApp/Scripts/Components/SidebarActions.razor.css new file mode 100644 index 0000000..9092fe3 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/SidebarActions.razor.css @@ -0,0 +1,9 @@ +.sidebar-actions { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 5px 5px; + border-bottom: 1px solid rgba(128, 128, 128, 0.3); +} diff --git a/RobotNet.WebApp/Scripts/Components/_Imports.razor b/RobotNet.WebApp/Scripts/Components/_Imports.razor new file mode 100644 index 0000000..41d2a31 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Components/_Imports.razor @@ -0,0 +1,3 @@ +@using RobotNet.Script.Shares +@using RobotNet.WebApp.Clients +@using RobotNet.WebApp.Scripts.Models diff --git a/RobotNet.WebApp/Scripts/Models/ConsoleItemModel.cs b/RobotNet.WebApp/Scripts/Models/ConsoleItemModel.cs new file mode 100644 index 0000000..767614b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ConsoleItemModel.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ConsoleItemModel(DiagnosticSeverity severity, string message) +{ + public string Message { get; private set; } = message; + public DiagnosticSeverity Severity { get; private set; } = severity; + + public event Action? Changed; + + public void Append(string message) + { + this.Message = $"{Message}{Environment.NewLine}{message}"; + Changed?.Invoke(); + } +} diff --git a/RobotNet.WebApp/Scripts/Models/HierachyItemModel.cs b/RobotNet.WebApp/Scripts/Models/HierachyItemModel.cs new file mode 100644 index 0000000..8207445 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/HierachyItemModel.cs @@ -0,0 +1,12 @@ +namespace RobotNet.WebApp.Scripts.Models; + +public interface IHierachyItemModel +{ + string Name { get; } + bool IsModified { get; } + int WarningCount { get; } + int ErrorCount { get; } + event Action? OnModified; + event Action? OnNameChanged; + event Action? OnDiagnosticsChanged; +} diff --git a/RobotNet.WebApp/Scripts/Models/InstanceMissionModel.cs b/RobotNet.WebApp/Scripts/Models/InstanceMissionModel.cs new file mode 100644 index 0000000..4baf217 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/InstanceMissionModel.cs @@ -0,0 +1,14 @@ +using RobotNet.Script.Shares; + +namespace RobotNet.WebApp.Scripts.Models; + +public class InstanceMissionModel(int index, InstanceMissionDto mission) +{ + public int Index { get; set; } = index; + public Guid Id { get; set; } = mission.Id; + public string MissionName { get; set; } = mission.MissionName; + public string Parameters { get; set; } = mission.Parameters; + public DateTime CreatedAt { get; set; } = mission.CreatedAt; + public MissionStatus Status { get; set; } = mission.Status; + public string? Log { get; set; } = mission.Log ?? string.Empty; +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptFileModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptFileModel.cs new file mode 100644 index 0000000..1ff90fa --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptFileModel.cs @@ -0,0 +1,80 @@ +using Microsoft.CodeAnalysis; +using RobotNet.Script.Shares; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptFileModel(DocumentId id, ScriptFile model, int level, ScriptFolderModel? parrent) : IHierachyItemModel +{ + public int Level { get; } = level; + public ScriptFile Model { get; private set; } = model; + public ScriptFolderModel? Parrent { get; } = parrent; + public string Name => Model.Name; + public string Code => Model.Code; + public string Path => System.IO.Path.Combine(Parrent?.Path ?? "", Model.Name); + + public DocumentId Id { get; } = id; + public bool IsModified { get; private set; } + public int WarningCount { get; private set; } + public int ErrorCount { get; private set; } + public string EditCode { get; private set; } = model.Code; + + public IEnumerable Diagnostics { get; private set; } = []; + + public event Action? OnModified; + public event Action? OnNameChanged; + public event Action? OnDiagnosticsChanged; + public void ChangeCode(string code) + { + EditCode = code; + if (IsModified) + { + if (EditCode == Code) + { + IsModified = false; + OnModified?.Invoke(); + } + } + else + { + if (EditCode != Code) + { + IsModified = true; + OnModified?.Invoke(); + } + } + } + + public void Saved() + { + if (!IsModified || EditCode == Code) return; + + Model = new ScriptFile(Name, EditCode); + IsModified = false; + OnModified?.Invoke(); + } + + public void Rename(string newName) + { + if (string.IsNullOrEmpty(newName)) throw new ArgumentNullException("Tên file không được để trống"); + + if (Name == newName) return; + + Model = new ScriptFile(newName, Code); + OnNameChanged?.Invoke(); + } + + public void SetDiagnostics(IEnumerable diagnostics) + { + Diagnostics = diagnostics; + var warning = Diagnostics.Count(d => d.Severity == DiagnosticSeverity.Warning); + var error = Diagnostics.Count(d => d.Severity == DiagnosticSeverity.Error); + + if(WarningCount != warning || ErrorCount != error) + { + OnDiagnosticsChanged?.Invoke(warning - WarningCount, error - ErrorCount); + WarningCount = warning; + ErrorCount = error; + } + + } +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptFolderModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptFolderModel.cs new file mode 100644 index 0000000..94053b9 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptFolderModel.cs @@ -0,0 +1,101 @@ +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptFolderModel(ScriptFolderModel? parrent, string name, int level) : IHierachyItemModel +{ + public int Level { get; } = level; + public ScriptFolderModel? Parrent { get; } = parrent; + public string Path => System.IO.Path.Combine(Parrent?.Path ?? "", Name); + + public readonly List Folders = []; + public readonly List Files = []; + + public bool IsModified { get; private set; } + public int WarningCount { get; private set; } + public int ErrorCount { get; private set; } + + public event Action? OnChildrenChanged; + public event Action? OnModified; + public event Action? OnNameChanged; + public event Action? OnDiagnosticsChanged; + + public string Name { get; private set; } = name; + + public void AddFiles(params IEnumerable files) + { + Files.AddRange(files); + Files.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + IsModified = IsModified || files.Any(file => file.IsModified); + WarningCount += files.Sum(folder => folder.WarningCount); + ErrorCount += files.Sum(folder => folder.ErrorCount); + + foreach (var file in files) + { + file.OnModified += UpdateModified; + file.OnDiagnosticsChanged += DiagnosticsChanged; + } + + OnModified?.Invoke(); + OnChildrenChanged?.Invoke(); + } + + public void AddFolders(params IEnumerable folders) + { + Folders.AddRange(folders); + Folders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + IsModified = IsModified || folders.Any(folder => folder.IsModified); + WarningCount += folders.Sum(folder => folder.WarningCount); + ErrorCount += folders.Sum(folder => folder.ErrorCount); + + foreach (var folder in folders) + { + folder.OnModified += UpdateModified; + folder.OnDiagnosticsChanged += DiagnosticsChanged; + } + + OnModified?.Invoke(); + OnChildrenChanged?.Invoke(); + } + + public void RemoveFile(ScriptFileModel file) + { + if(Files.Remove(file)) + { + OnChildrenChanged?.Invoke(); + } + } + + public void RemoveFolder(ScriptFolderModel folder) + { + if(Folders.Remove(folder)) + { + OnChildrenChanged?.Invoke(); + } + } + + public void Rename(string newName) + { + if(string.IsNullOrEmpty(newName)) throw new ArgumentNullException("Tên thư mục không được để trống"); + + if(Name == newName) return; + + Name = newName; + OnNameChanged?.Invoke(); + } + + private void UpdateModified() + { + IsModified = Files.Any(file => file.IsModified) || Folders.Any(folder => folder.IsModified); + OnModified?.Invoke(); + } + + private void DiagnosticsChanged(int warningCount, int errorCount) + { + WarningCount += warningCount; + ErrorCount += errorCount; + //WarningCount = Files.Sum(file => file.WarningCount) + Folders.Sum(folder => folder.WarningCount); + //ErrorCount = Files.Sum(file => file.ErrorCount) + Folders.Sum(folder => folder.ErrorCount); + OnDiagnosticsChanged?.Invoke(warningCount, errorCount); + } +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptMissionModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptMissionModel.cs new file mode 100644 index 0000000..401e0b8 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptMissionModel.cs @@ -0,0 +1,11 @@ +using RobotNet.Script.Shares; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptMissionModel(int index, ScriptMissionDto data) +{ + public int Index { get; } = index; + public string Name { get; } = data.Name; + public string Code { get; } = data.Code; + public IEnumerable Parameters { get; } = [.. data.Parameters.Select(p => new ScriptValueModel(p.Name, p.Type, p.Default ?? ""))]; +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptMissionParameterModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptMissionParameterModel.cs new file mode 100644 index 0000000..8b47cf3 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptMissionParameterModel.cs @@ -0,0 +1,247 @@ +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptValueModel(string name, string type, string valueDefault) +{ + private static readonly Dictionary predefinedMap = new() + { + ["System.Boolean"] = typeof(bool), + ["System.Byte"] = typeof(byte), + ["System.SByte"] = typeof(sbyte), + ["System.Int16"] = typeof(short), + ["System.UInt16"] = typeof(ushort), + ["System.Int32"] = typeof(int), + ["System.UInt32"] = typeof(uint), + ["System.Int64"] = typeof(long), + ["System.UInt64"] = typeof(ulong), + ["System.Single"] = typeof(float), + ["System.Double"] = typeof(double), + ["System.Decimal"] = typeof(decimal), + ["System.Char"] = typeof(char), + ["System.String"] = typeof(string) + }; + + public string Name { get; } = name; + public string Type { get; } = type; + public string? Default { get; } = valueDefault; + + public object? Value { get; set; } = null; + + private void EnsureType(string expectedType) + { + if (Type != expectedType) + throw new InvalidOperationException($"Parameter '{Name}' is not of type '{expectedType}'. Actual type: '{Type}'."); + } + + public override string ToString() + { + return Value?.ToString() ?? "null"; + } + + public bool BoolValue + { + get + { + EnsureType("System.Boolean"); + return Value is not null && (bool)Value; + } + set + { + EnsureType("System.Boolean"); + Value = value; + } + } + + public byte ByteValue + { + get + { + EnsureType("System.Byte"); + return Value is null ? default : (byte)Value; + } + set + { + EnsureType("System.Byte"); + Value = value; + } + } + + public sbyte SByteValue + { + get + { + EnsureType("System.SByte"); + return Value is null ? default : (sbyte)Value; + } + set + { + EnsureType("System.SByte"); + Value = value; + } + } + + public short ShortValue + { + get + { + EnsureType("System.Int16"); + return Value is null ? default : (short)Value; + } + set + { + EnsureType("System.Int16"); + Value = value; + } + } + + public ushort UShortValue + { + get + { + EnsureType("System.UInt16"); + return Value is null ? default : (ushort)Value; + } + set + { + EnsureType("System.UInt16"); + Value = value; + } + } + + public int IntValue + { + get + { + EnsureType("System.Int32"); + return Value is null ? default : (int)Value; + } + set + { + EnsureType("System.Int32"); + Value = value; + } + } + + public uint UIntValue + { + get + { + EnsureType("System.UInt32"); + return Value is null ? default : (uint)Value; + } + set + { + EnsureType("System.UInt32"); + Value = value; + } + } + + public long LongValue + { + get + { + EnsureType("System.Int64"); + return Value is null ? default : (long)Value; + } + set + { + EnsureType("System.Int64"); + Value = value; + } + } + + public ulong ULongValue + { + get + { + EnsureType("System.UInt64"); + return Value is null ? default : (ulong)Value; + } + set + { + EnsureType("System.UInt64"); + Value = value; + } + } + + public float FloatValue + { + get + { + EnsureType("System.Single"); + return Value is null ? default : (float)Value; + } + set + { + EnsureType("System.Single"); + Value = value; + } + } + + public double DoubleValue + { + get + { + EnsureType("System.Double"); + return Value is null ? default : (double)Value; + } + set + { + EnsureType("System.Double"); + Value = value; + } + } + + public decimal DecimalValue + { + get + { + EnsureType("System.Decimal"); + return Value is null ? default : (decimal)Value; + } + set + { + EnsureType("System.Decimal"); + Value = value; + } + } + + public char CharValue + { + get + { + EnsureType("System.Char"); + return Value is null ? default : (char)Value; + } + set + { + EnsureType("System.Char"); + Value = value; + } + } + + public string StringValue + { + get + { + EnsureType("System.String"); + return Value is null ? string.Empty : (string)Value; + } + set + { + EnsureType("System.String"); + Value = value; + } + } + + public void Reset() + { + if (predefinedMap.TryGetValue(Type, out var type)) + { + Value = type.IsValueType ? Activator.CreateInstance(type) : (type == typeof(string) ? string.Empty : null); + } + else + { + Value = null; + } + } +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptTaskModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptTaskModel.cs new file mode 100644 index 0000000..d6416c6 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptTaskModel.cs @@ -0,0 +1,12 @@ +using RobotNet.Script.Shares; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptTaskModel(int index, ScriptTaskDto data) +{ + public int Index { get; } = index; + public string Name { get; } = data.Name; + public int Interval { get; } = data.Interval; + public bool Enable { get; set; } + public string Code { get; } = data.Code; +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptVariableModel.cs b/RobotNet.WebApp/Scripts/Models/ScriptVariableModel.cs new file mode 100644 index 0000000..3559874 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptVariableModel.cs @@ -0,0 +1,11 @@ +using RobotNet.Script.Shares; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptVariableModel(int index, ScriptVariableDto variableDto) +{ + public int Index { get; } = index; + public string Name { get; } = variableDto.Name; + public string TypeName { get; } = variableDto.TypeName; + public string Value { get; } = variableDto.Value; +} diff --git a/RobotNet.WebApp/Scripts/Models/ScriptWorkspace.cs b/RobotNet.WebApp/Scripts/Models/ScriptWorkspace.cs new file mode 100644 index 0000000..11cb8f2 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Models/ScriptWorkspace.cs @@ -0,0 +1,561 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.QuickInfo; +using Microsoft.CodeAnalysis.Text; +using MudBlazor; +using RobotNet.Script.Shares; +using RobotNet.Shares; +using RobotNet.Clients; +using RobotNet.WebApp.Scripts.Monaco; +using RobotNet.WebApp.Scripts.Monaco.Languages; +using System.Net.Http.Json; +using System.Text; +using System.Timers; + +namespace RobotNet.WebApp.Scripts.Models; + +public class ScriptWorkspace(IHttpClientFactory httpFactory) : IDisposable +{ + private static readonly CSharpParseOptions cSharpParse = CSharpParseOptions.Default.WithKind(SourceCodeKind.Script).WithLanguageVersion(LanguageVersion.Latest); + private readonly AdhocWorkspace adhocWorkspace = new(); + private readonly ProjectId EditorProjectId = ProjectId.CreateNewId(); + + public readonly List Folders = []; + public readonly List Files = []; + public ScriptFolderModel? SelectedFolder { get; private set; } + public ScriptFileModel? SelectedFile { get; private set; } + public ScriptFileModel? CurrentFile { get; private set; } + + public ProcessorState ProcessorState { get; set; } + public bool IsReadOnly => ProcessorState != ProcessorState.Ready && ProcessorState != ProcessorState.Idle && ProcessorState != ProcessorState.BuildError; + + public event Func? OnFileChanged; + public event Func, Task>? OnDiagnoticChanged; + public event Action? RootChanged; + + private readonly HttpClient http = httpFactory.CreateClient("ScriptManagerAPI"); + + private readonly System.Timers.Timer DiagnosticTimer = new() + { + AutoReset = false, + Interval = 1000, + }; + + private bool IsInitialized = false; + public async Task InitializeAsync() + { + if (IsInitialized) return; + + IsInitialized = true; + + var collectionsDllStream = await http.GetStreamAsync($"dlls/System.Collections.dll"); + var runtimeDllStream = await http.GetStreamAsync($"dlls/System.Runtime.dll"); + var expressionsDllStream = await http.GetStreamAsync($"dlls/System.Linq.Expressions.dll"); + var robotNetDllStream = await http.GetStreamAsync($"dlls/RobotNet.Script.dll"); + var robotNetDocBuf = await http.GetByteArrayAsync($"dlls/RobotNet.Script.xml"); + var robotNetExpressionsDllStream = await http.GetStreamAsync($"dlls/RobotNet.Script.Expressions.dll"); + var robotNetExpressionsDocBuf = await http.GetByteArrayAsync($"dlls/RobotNet.Script.Expressions.xml"); + + var usingNamespaces = await http.GetFromJsonAsync>("api/Script/UsingNamespaces") ?? []; + var preCode = await http.GetFromJsonAsync("api/Script/PreCode") ?? ""; + + var projectInfo = ProjectInfo.Create(EditorProjectId, VersionStamp.Create(), "EditorProject", "EditorAssembly", LanguageNames.CSharp) + .WithMetadataReferences([ + MetadataReference.CreateFromStream(collectionsDllStream, MetadataReferenceProperties.Assembly), + MetadataReference.CreateFromStream(runtimeDllStream, MetadataReferenceProperties.Assembly), + MetadataReference.CreateFromStream(expressionsDllStream, MetadataReferenceProperties.Assembly), + MetadataReference.CreateFromStream(robotNetDllStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(robotNetDocBuf)), + MetadataReference.CreateFromStream(robotNetExpressionsDllStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(robotNetExpressionsDocBuf)), + ]) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, usings: usingNamespaces)) + .WithParseOptions(cSharpParse); + + Solution updatedSolution = adhocWorkspace.CurrentSolution.AddProject(projectInfo); + + updatedSolution = updatedSolution.AddDocument(DocumentId.CreateNewId(EditorProjectId), "PreScript.cs", SourceText.From(preCode), ["/"], "/PreScript.cs"); + + if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Khởi tạo project thất bại"); + + Folders.Clear(); + Files.Clear(); + + var rootFolder = await http.GetFromJsonAsync("api/Script") ?? new ScriptFolder("Script", [], []); + + Files.AddRange([.. rootFolder.Files.Select(file => CreateFileModel(file, 0, null))]); + Files.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + Folders.AddRange([.. rootFolder.Folders.Select(folder => CreateFolderModel(folder, 0, null))]); + Folders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + + var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId); + if (editorProject == null) return; + + var compilation = await editorProject.GetCompilationAsync(); + var diagnostics = compilation?.GetDiagnostics() ?? []; + + foreach (var file in Files) + { + await GetDiagnosticsToModel(editorProject, diagnostics, file); + } + + foreach (var folder in Folders) + { + await GetDiagnosticsToModel(editorProject, diagnostics, folder); + } + + DiagnosticTimer.Elapsed += DiagnosticTimer_Elapsed; + + RootChanged?.Invoke(); + } + + private async void DiagnosticTimer_Elapsed(object? sender, ElapsedEventArgs e) + { + var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId); + if (editorProject == null) return; + + var compilation = await editorProject.GetCompilationAsync(); + var diagnostics = compilation?.GetDiagnostics() ?? []; + + foreach (var file in Files) + { + await GetDiagnosticsToModel(editorProject, diagnostics, file); + } + + foreach (var folder in Folders) + { + await GetDiagnosticsToModel(editorProject, diagnostics, folder); + } + + if (OnDiagnoticChanged is not null && CurrentFile is not null) + { + await OnDiagnoticChanged.Invoke(CurrentFile.Diagnostics); + } + } + + private static async Task GetDiagnosticsToModel(Project project, IEnumerable diagnostics, ScriptFileModel file) + { + var document = project.Solution.GetDocument(file.Id); + if (document == null) return; + + var syntaxTree = await document.GetSyntaxTreeAsync(); + if (syntaxTree == null) return; + + file.SetDiagnostics(diagnostics.Where(d => d.Location.IsInSource && d.Location.SourceTree == syntaxTree)); + } + + private static async Task GetDiagnosticsToModel(Project project, IEnumerable diagnostics, ScriptFolderModel folders) + { + foreach (var file in folders.Files) + { + await GetDiagnosticsToModel(project, diagnostics, file); + } + foreach (var folder in folders.Folders) + { + await GetDiagnosticsToModel(project, diagnostics, folder); + } + } + + private ScriptFileModel CreateFileModel(ScriptFile file, int level, ScriptFolderModel? parrent) + { + Solution updatedSolution = adhocWorkspace.CurrentSolution; + var newId = DocumentId.CreateNewId(EditorProjectId); + + updatedSolution = updatedSolution.AddDocument(newId, file.Name, SourceText.From(file.Code), parrent?.Path.Split("/"), Path.Combine(parrent?.Path ?? "", file.Name)); + + if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Tạo document mới thất bại"); + + var model = new ScriptFileModel(newId, file, level, parrent); + + return model; + } + + private ScriptFolderModel CreateFolderModel(ScriptFolder folder, int level, ScriptFolderModel? parrent) + { + var model = new ScriptFolderModel(parrent, folder.Name, level); + model.AddFiles([.. folder.Files.Select(file => CreateFileModel(file, level + 1, model))]); + model.AddFolders([.. folder.Folders.Select(dir => CreateFolderModel(dir, level + 1, model))]); + return model; + } + + public bool SelectFolder(ScriptFolderModel? folder) + { + if (SelectedFolder != folder) + { + SelectedFolder = folder; + SelectedFile = null; + return true; + } + else + { + return false; + } + } + + public async Task SelectFile(ScriptFileModel? file) + { + CurrentFile = file; + SelectedFile = CurrentFile; + SelectedFolder = file?.Parrent; + + if (OnFileChanged is not null) + { + await OnFileChanged.Invoke(file); + } + } + + + public async Task CreateNewFile(string name) + { + var result = await http.PostFromJsonAsync("api/Script/File", new CreateModel(SelectedFolder?.Path ?? "", name)); + if (result is null) + { + return new(false, "Không thể tạo file mới"); + } + + if (result.IsSuccess) + { + var file = new ScriptFile(name, ""); + if (SelectedFolder is null) + { + Files.Add(CreateFileModel(file, 0, null)); + Files.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + RootChanged?.Invoke(); + } + else + { + SelectedFolder.AddFiles(CreateFileModel(file, SelectedFolder.Level + 1, SelectedFolder)); + } + } + + return result; + } + + public async Task CreateNewFolder(string name) + { + var result = await http.PostFromJsonAsync("api/Script/Directory", new CreateModel(SelectedFolder?.Path ?? "", name)); + if (result is null) + { + return new(false, "Không thể tạo thư mục mới"); + } + + if (result.IsSuccess) + { + var folder = new ScriptFolder(name, [], []); + if (SelectedFolder is null) + { + Folders.Add(CreateFolderModel(folder, 0, null)); + Folders.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); + RootChanged?.Invoke(); + } + else + { + SelectedFolder.AddFolders(CreateFolderModel(folder, SelectedFolder.Level + 1, SelectedFolder)); + } + } + + return result; + } + + public void WriteDocument(string text) + { + if (CurrentFile is null) return; + + Solution updatedSolution = adhocWorkspace.CurrentSolution; + updatedSolution = updatedSolution.WithDocumentText(CurrentFile.Id, SourceText.From(text)); + if (!adhocWorkspace.TryApplyChanges(updatedSolution)) throw new InvalidOperationException("Cập nhật project ban đầu thất bại"); + + CurrentFile.ChangeCode(text); + DiagnosticTimer.Stop(); + DiagnosticTimer.Start(); + } + + public async Task SaveCurrentFileAsync() + { + if (CurrentFile is null || !CurrentFile.IsModified || CurrentFile.EditCode == CurrentFile.Code) return new(true, ""); + + var result = await http.PatchFromJsonAsync("api/Script/File", new UpdateModel(CurrentFile.Path, CurrentFile.EditCode)); + if (result is null) + { + return new(false, "Không thể update được code"); + } + + if (result.IsSuccess) + { + CurrentFile.Saved(); + } + + return result; + } + + public async Task> SaveSelectAsync() + { + if (SelectedFolder is not null) + { + var results = new List(); + foreach (var subFolder in SelectedFolder.Folders) + { + results.AddRange(await SaveAllAsync(subFolder)); + } + results.AddRange(await SaveAllAsync(Files)); + return results; + } + else if (SelectedFile is not null) + { + return await SaveAllAsync([SelectedFile]); + } + else + { + return []; + } + } + + public async Task> SaveAllAsync() + { + var results = new List(); + foreach (var subFolder in Folders) + { + results.AddRange(await SaveAllAsync(subFolder)); + } + results.AddRange(await SaveAllAsync(Files)); + return results; + } + + private async Task> SaveAllAsync(IEnumerable files) + { + var results = new List(); + foreach (var file in files) + { + if (!file.IsModified || file.EditCode == file.Code) continue; + var result = await http.PatchFromJsonAsync("api/Script/File", new UpdateModel(file.Path, file.EditCode)); + if (result is null) + { + results.Add(new(false, $"Không thể update được code của {file.Name}")); + continue; + } + if (result.IsSuccess) + { + file.Saved(); + } + results.Add(result); + } + return results; + } + + + private async Task> SaveAllAsync(ScriptFolderModel folder) + { + var results = new List(); + results.AddRange(await SaveAllAsync(folder.Files)); + foreach (var subFolder in folder.Folders) + { + results.AddRange(await SaveAllAsync(subFolder)); + } + return results; + } + + public async Task DeleteFile(ScriptFileModel file) + { + var result = await http.DeleteFromJsonAsync($"api/Script/File?path={file.Path}"); + if (result?.IsSuccess ?? false) + { + if (Files.Remove(file)) + { + RootChanged?.Invoke(); + } + else + { + file.Parrent?.RemoveFile(file); + } + if (SelectedFile == file) + { + await SelectFile(null); + } + + Solution updatedSolution = adhocWorkspace.CurrentSolution; + + updatedSolution = updatedSolution.RemoveDocument(file.Id); + + if (!adhocWorkspace.TryApplyChanges(updatedSolution)) + { + return new(false, "Xóa document trong workspace mới thất bại"); + } + + DiagnosticTimer.Stop(); + DiagnosticTimer.Start(); + } + return result ?? new MessageResult(false, "Lỗi server response"); + } + + public async Task DeleteFolder(ScriptFolderModel folder) + { + var result = await http.DeleteFromJsonAsync($"api/Script/Directory?path={folder.Path}"); + if (result?.IsSuccess ?? false) + { + if (Folders.Remove(folder)) + { + RootChanged?.Invoke(); + } + else + { + folder.Parrent?.RemoveFolder(folder); + } + Solution updatedSolution = adhocWorkspace.CurrentSolution; + await RemoveFolderFromWorkspace(updatedSolution, folder); + if (!adhocWorkspace.TryApplyChanges(updatedSolution)) + { + return new(false, "Xóa document trong workspace mới thất bại"); + } + + DiagnosticTimer.Stop(); + DiagnosticTimer.Start(); + + } + return result ?? new MessageResult(false, "Lỗi server response"); + } + + private async Task RemoveFolderFromWorkspace(Solution updatedSolution, ScriptFolderModel folder) + { + foreach (var dir in folder.Folders) + { + await RemoveFolderFromWorkspace(updatedSolution, dir); + } + foreach (var file in folder.Files) + { + if (SelectedFile == file) + { + _ = SelectFile(null); + } + updatedSolution = updatedSolution.RemoveDocument(file.Id); + } + } + + public async Task Rename(ScriptFolderModel folder, string newName) + { + var result = await http.PatchFromJsonAsync("api/Script/DirectoryName", new RenameModel(folder.Path, newName)); + if (result?.IsSuccess ?? false) + { + folder.Rename(newName); + } + + return result ?? new MessageResult(false, "Lỗi server response"); + } + + public async Task Rename(ScriptFileModel file, string newName) + { + var result = await http.PatchFromJsonAsync("api/Script/FileName", new RenameModel(file.Path, newName)); + if (result?.IsSuccess ?? false) + { + file.Rename(newName); + } + + return result ?? new MessageResult(false, "Lỗi server response"); + } + + public bool AnyChanges() => AnyChanges(Files) || Folders.Any(AnyChanges); + private static bool AnyChanges(ScriptFolderModel folder) => AnyChanges(folder.Files) || folder.Folders.Any(AnyChanges); + private static bool AnyChanges(IEnumerable files) => files.Any(file => file.IsModified); + + public async Task> GetAllDiagnostics() + { + var editorProject = adhocWorkspace.CurrentSolution.GetProject(EditorProjectId); + if (editorProject == null) return []; + + var compilation = await editorProject.GetCompilationAsync(); + + return compilation?.GetDiagnostics() ?? []; + } + + public async Task> GetCompletionCurrentFileAsync(int line, int column, int kind, char? triggerCharacter) + { + return CurrentFile is null ? [] : await adhocWorkspace.GetCompletionAsync(CurrentFile.Id, line, column, kind, triggerCharacter); + } + + + public async Task GetQuickInfoCurrentFile(int line, int column) + { + if (CurrentFile is null) return null; + + var document = adhocWorkspace.CurrentSolution.GetDocument(CurrentFile.Id); + if (document is null) return null; + + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines.GetPosition(new LinePosition(line - 1, column - 1)); + + var quickInfoService = QuickInfoService.GetService(document); + if (quickInfoService is null) return string.Empty; + + var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position); + if (quickInfo is null) return string.Empty; + + var finalTextBuilder = new StringBuilder(); + + bool lastSectionHadLineBreak = true; + var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description); + if (description is not null) + { + finalTextBuilder.AppendSection(description, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak); + } + + var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments); + if (summary is not null) + { + finalTextBuilder.AppendSection(summary, MarkdownFormat.Default, ref lastSectionHadLineBreak); + } + + foreach (var section in quickInfo.Sections) + { + switch (section.Kind) + { + case QuickInfoSectionKinds.Description: + case QuickInfoSectionKinds.DocumentationComments: + continue; + + case QuickInfoSectionKinds.TypeParameters: + finalTextBuilder.AppendSection(section, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak); + break; + + case QuickInfoSectionKinds.AnonymousTypes: + // The first line is "Anonymous Types:" + // Then we want all anonymous types to be C# highlighted + finalTextBuilder.AppendSection(section, MarkdownFormat.FirstLineDefaultRestCSharp, ref lastSectionHadLineBreak); + break; + + case "NullabilityAnalysis": + // Italicize the nullable analysis for emphasis. + finalTextBuilder.AppendSection(section, MarkdownFormat.Italicize, ref lastSectionHadLineBreak); + break; + + default: + finalTextBuilder.AppendSection(section, MarkdownFormat.Default, ref lastSectionHadLineBreak); + break; + } + } + + return finalTextBuilder.ToString().Trim(); + } + + public async Task GetSignatureHelpCurrentFile(int line, int column) + { + if (CurrentFile is null) return null; + + var document = adhocWorkspace.CurrentSolution.GetDocument(CurrentFile.Id); + if (document is null) return null; + + return await document.GetSignatureHelpAsync(line, column); + } + + private bool isDisposed = false; + public void Dispose() + { + if (isDisposed) throw new NotImplementedException(); + + isDisposed = true; + + DiagnosticTimer.Elapsed -= DiagnosticTimer_Elapsed; + DiagnosticTimer.Stop(); + + adhocWorkspace.Dispose(); + DiagnosticTimer.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationComment.cs b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationComment.cs new file mode 100644 index 0000000..375d3cd --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationComment.cs @@ -0,0 +1,196 @@ +using System.Text; +using System.Xml; + +namespace RobotNet.WebApp.Scripts.Monaco.Documentation; + +public class DocumentationComment( + string summaryText = "", + DocumentationItem[]? typeParamElements = null, + DocumentationItem[]? paramElements = null, + string returnsText = "", + string remarksText = "", + string exampleText = "", + string valueText = "", + DocumentationItem[]? exception = null) +{ + public string SummaryText { get; } = summaryText; + public DocumentationItem[] TypeParamElements { get; } = typeParamElements ?? []; + public DocumentationItem[] ParamElements { get; } = paramElements ?? []; + public string ReturnsText { get; } = returnsText; + public string RemarksText { get; } = remarksText; + public string ExampleText { get; } = exampleText; + public string ValueText { get; } = valueText; + public DocumentationItem[] Exception { get; } = exception ?? []; + + public static DocumentationComment? From(string xmlDocumentation, string lineEnding) + { + if (string.IsNullOrEmpty(xmlDocumentation)) + return Empty; + + var reader = new StringReader("" + xmlDocumentation + ""); + var summaryText = new StringBuilder(); + var typeParamElements = new List(); + var paramElements = new List(); + var returnsText = new StringBuilder(); + var remarksText = new StringBuilder(); + var exampleText = new StringBuilder(); + var valueText = new StringBuilder(); + var exception = new List(); + + using (var xml = XmlReader.Create(reader)) + { + try + { + xml.Read(); + string? elementName = null; + StringBuilder? currentSectionBuilder = null; + do + { + if (xml.NodeType == XmlNodeType.Element) + { + elementName = xml.Name.ToLowerInvariant(); + switch (elementName) + { + case "filterpriority": + xml.Skip(); + break; + case "remarks": + currentSectionBuilder = remarksText; + break; + case "example": + currentSectionBuilder = exampleText; + break; + case "exception": + DocumentationItemBuilder exceptionInstance = new(GetCref(xml["cref"]).TrimEnd()); + currentSectionBuilder = exceptionInstance.Documentation; + exception.Add(exceptionInstance); + break; + case "returns": + currentSectionBuilder = returnsText; + break; + case "summary": + currentSectionBuilder = summaryText; + break; + case "see": + if (currentSectionBuilder is null) continue; + currentSectionBuilder.Append(GetCref(xml["cref"])); + currentSectionBuilder.Append(xml["langword"]); + break; + case "seealso": + if (currentSectionBuilder is null) continue; + currentSectionBuilder.Append("See also: "); + currentSectionBuilder.Append(GetCref(xml["cref"])); + break; + case "paramref": + if (currentSectionBuilder is null) continue; + currentSectionBuilder.Append(xml["name"]); + currentSectionBuilder.Append(' '); + break; + case "param": + + DocumentationItemBuilder paramInstance = new(TrimMultiLineString(xml["name"] ?? "", lineEnding)); + currentSectionBuilder = paramInstance.Documentation; + paramElements.Add(paramInstance); + break; + case "typeparamref": + if (currentSectionBuilder is null) continue; + currentSectionBuilder.Append(xml["name"]); + currentSectionBuilder.Append(' '); + break; + case "typeparam": + DocumentationItemBuilder typeParamInstance = new(TrimMultiLineString(xml["name"] ?? "", lineEnding)); + currentSectionBuilder = typeParamInstance.Documentation; + typeParamElements.Add(typeParamInstance); + break; + case "value": + currentSectionBuilder = valueText; + break; + case "br": + case "para": + if (currentSectionBuilder is null) continue; + currentSectionBuilder.Append(lineEnding); + break; + } + } + else if (xml.NodeType == XmlNodeType.Text && currentSectionBuilder != null) + { + if (elementName == "code") + { + currentSectionBuilder.Append(xml.Value); + } + else + { + currentSectionBuilder.Append(TrimMultiLineString(xml.Value, lineEnding)); + } + } + } while (xml.Read()); + } + catch (Exception) + { + return null; + } + } + + return new DocumentationComment( + summaryText.ToString(), + typeParamElements.Select(s => s.ConvertToDocumentedObject()).ToArray(), + paramElements.Select(s => s.ConvertToDocumentedObject()).ToArray(), + returnsText.ToString(), + remarksText.ToString(), + exampleText.ToString(), + valueText.ToString(), + exception.Select(s => s.ConvertToDocumentedObject()).ToArray()); + } + + private static string TrimMultiLineString(string input, string lineEnding) + { + var lines = input.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return string.Join(lineEnding, lines.Select(l => TrimStartRetainingSingleLeadingSpace(l))); + } + + private static string GetCref(string? cref) + { + if (cref == null || cref.Trim().Length == 0) + { + return ""; + } + if (cref.Length < 2) + { + return cref; + } + if (cref.Substring(1, 1) == ":") + { + return string.Concat(cref.AsSpan(2, cref.Length - 2), " "); + } + return cref + " "; + } + + private static string TrimStartRetainingSingleLeadingSpace(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return string.Empty; + if (!char.IsWhiteSpace(input[0])) + return input; + return $" {input.TrimStart()}"; + } + + public string GetParameterText(string name) + => Array.Find(ParamElements, parameter => parameter.Name == name)?.Documentation ?? string.Empty; + + public string GetTypeParameterText(string name) + => Array.Find(TypeParamElements, typeParam => typeParam.Name == name)?.Documentation ?? string.Empty; + + public static readonly DocumentationComment Empty = new(); + private static readonly string[] separator = ["\n", "\r\n"]; +} + +class DocumentationItemBuilder(string name) +{ + public string Name { get; set; } = name; + public StringBuilder Documentation { get; set; } = new StringBuilder(); + + public DocumentationItem ConvertToDocumentedObject() + { + return new DocumentationItem(Name, Documentation.ToString()); + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationConverter.cs b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationConverter.cs new file mode 100644 index 0000000..fd0f17d --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationConverter.cs @@ -0,0 +1,169 @@ +using Microsoft.CodeAnalysis; +using System.Text; +using System.Xml; + +namespace RobotNet.WebApp.Scripts.Monaco.Documentation; + +public class DocumentationConverter +{/// + /// Converts the xml documentation string into a plain text string. + /// + public static string ConvertDocumentation(string xmlDocumentation, string lineEnding) + { + if (string.IsNullOrEmpty(xmlDocumentation)) + return string.Empty; + + var reader = new StringReader("" + xmlDocumentation + ""); + using var xml = XmlReader.Create(reader); + var ret = new StringBuilder(); + + try + { + xml.Read(); + string? elementName = null; + do + { + if (xml.NodeType == XmlNodeType.Element) + { + elementName = xml.Name.ToLowerInvariant(); + switch (elementName) + { + case "filterpriority": + xml.Skip(); + break; + case "remarks": + ret.Append(lineEnding); + ret.Append("Remarks:"); + ret.Append(lineEnding); + break; + case "example": + ret.Append(lineEnding); + ret.Append("Example:"); + ret.Append(lineEnding); + break; + case "exception": + ret.Append(lineEnding); + ret.Append(GetCref(xml["cref"]).TrimEnd()); + ret.Append(": "); + break; + case "returns": + ret.Append(lineEnding); + ret.Append("Returns: "); + break; + case "see": + ret.Append(GetCref(xml["cref"])); + ret.Append(xml["langword"]); + break; + case "seealso": + ret.Append(lineEnding); + ret.Append("See also: "); + ret.Append(GetCref(xml["cref"])); + break; + case "paramref": + ret.Append(xml["name"]); + ret.Append(' '); + break; + case "typeparam": + ret.Append(lineEnding); + ret.Append('<'); + ret.Append(TrimMultiLineString(xml["name"], lineEnding)); + ret.Append(">: "); + break; + case "param": + ret.Append(lineEnding); + ret.Append(TrimMultiLineString(xml["name"], lineEnding)); + ret.Append(": "); + break; + case "value": + ret.Append(lineEnding); + ret.Append("Value: "); + ret.Append(lineEnding); + break; + case "br": + case "para": + ret.Append(lineEnding); + break; + } + } + else if (xml.NodeType == XmlNodeType.Text) + { + if (elementName == "code") + { + ret.Append(xml.Value); + } + else + { + ret.Append(TrimMultiLineString(xml.Value, lineEnding)); + } + } + } while (xml.Read()); + } + catch (Exception) + { + return xmlDocumentation; + } + return ret.ToString(); + } + + private static readonly string[] separator = ["\n", "\r\n"]; + + private static string TrimMultiLineString(string? input, string lineEnding) + { + if (string.IsNullOrEmpty(input)) return ""; + var lines = input.Split(separator, StringSplitOptions.RemoveEmptyEntries); + return string.Join(lineEnding, lines.Select(l => l.TrimStart())); + } + + private static string GetCref(string? cref) + { + if (cref == null || cref.Trim().Length == 0) + { + return ""; + } + if (cref.Length < 2) + { + return cref; + } + if (cref.Substring(1, 1) == ":") + { + return cref[2..] + " "; + } + return cref + " "; + } + + public static DocumentationComment? GetStructuredDocumentation(string? xmlDocumentation, string lineEnding) + { + if (string.IsNullOrEmpty(xmlDocumentation)) return null; + return DocumentationComment.From(xmlDocumentation, lineEnding); + } + + public static DocumentationComment? GetStructuredDocumentation(ISymbol symbol, string lineEnding = "\n") + { + return symbol switch + { + IParameterSymbol parameter => new DocumentationComment(summaryText: GetParameterDocumentation(parameter, lineEnding) ?? ""), + ITypeParameterSymbol typeParam => new DocumentationComment(summaryText: GetTypeParameterDocumentation(typeParam, lineEnding) ?? ""), + IAliasSymbol alias => new DocumentationComment(summaryText: GetAliasDocumentation(alias, lineEnding) ?? ""), + _ => GetStructuredDocumentation(symbol.GetDocumentationCommentXml(), lineEnding), + }; + } + + private static string? GetParameterDocumentation(IParameterSymbol parameter, string lineEnding = "\n") + { + var contaningSymbolDef = parameter.ContainingSymbol.OriginalDefinition; + return GetStructuredDocumentation(contaningSymbolDef.GetDocumentationCommentXml(), lineEnding) + ?.GetParameterText(parameter.Name); + } + + private static string? GetTypeParameterDocumentation(ITypeParameterSymbol typeParam, string lineEnding = "\n") + { + var contaningSymbol = typeParam.ContainingSymbol; + return GetStructuredDocumentation(contaningSymbol.GetDocumentationCommentXml(), lineEnding) + ?.GetTypeParameterText(typeParam.Name); + } + + private static string? GetAliasDocumentation(IAliasSymbol alias, string lineEnding = "\n") + { + return GetStructuredDocumentation(alias.Target.GetDocumentationCommentXml(), lineEnding)?.SummaryText; + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationItem.cs b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationItem.cs new file mode 100644 index 0000000..88a3727 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Documentation/DocumentationItem.cs @@ -0,0 +1,7 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Documentation; + +public class DocumentationItem(string name, string documentation) +{ + public string Name { get; } = name; + public string Documentation { get; } = documentation; +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Editor/MarkerData.cs b/RobotNet.WebApp/Scripts/Monaco/Editor/MarkerData.cs new file mode 100644 index 0000000..58e46ce --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Editor/MarkerData.cs @@ -0,0 +1,16 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Editor; + +public struct MarkerData +{ + public string Code { get; set; } + public int EndColumn { get; set; } + public int EndLineNumber { get; set; } + public string Message { get; set; } + public int ModelVersionId { get; set; } + public RelatedInformation[] RelatedInformation { get; set; } + public MarkerSeverity Severity { get; set; } + public string Source { get; set; } + public int StartColumn { get; set; } + public int StartLineNumber { get; set; } + public MarkerData[] Tags { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChange.cs b/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChange.cs new file mode 100644 index 0000000..ed075cd --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChange.cs @@ -0,0 +1,9 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Editor; + +public struct ModelContentChange +{ + public Range Range { get; set; } + public int RangeLength { get; set; } + public int RangeOffset { get; set; } + public string Text { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChangedEvent.cs b/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChangedEvent.cs new file mode 100644 index 0000000..64fc078 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Editor/ModelContentChangedEvent.cs @@ -0,0 +1,12 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Editor; + +public struct ModelContentChangedEvent +{ + public ModelContentChange[] Changes { get; set; } + public string Eol { get; set; } + public bool IsEolChange { get; set; } + public bool IsFlush { get; set; } + public bool IsRedoing { get; set; } + public bool IsUndoing { get; set; } + public int VersionId { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Editor/RelatedInformation.cs b/RobotNet.WebApp/Scripts/Monaco/Editor/RelatedInformation.cs new file mode 100644 index 0000000..768579b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Editor/RelatedInformation.cs @@ -0,0 +1,11 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Editor; + +public struct RelatedInformation +{ + public int EndColumn { get; set; } + public int EndLineNumber { get; set; } + public string Message { get; set; } + public string Resource { get; set; } + public int StartColumn { get; set; } + public int StartLineNumber { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Editor/SingleEditOperation.cs b/RobotNet.WebApp/Scripts/Monaco/Editor/SingleEditOperation.cs new file mode 100644 index 0000000..f86a216 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Editor/SingleEditOperation.cs @@ -0,0 +1,8 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Editor; + +public struct SingleEditOperation +{ + public bool? ForceMoveMarkers { get; set; } + public Range Range { get; set; } + public string Text { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/InvocationContext.cs b/RobotNet.WebApp/Scripts/Monaco/InvocationContext.cs new file mode 100644 index 0000000..3244cf8 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/InvocationContext.cs @@ -0,0 +1,34 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; + +namespace RobotNet.WebApp.Scripts.Monaco; + +public class InvocationContext +{ + public SemanticModel SemanticModel { get; } + public int Position { get; } + public SyntaxNode Receiver { get; } + public IEnumerable ArgumentTypes { get; } + public IEnumerable Separators { get; } + public bool IsInStaticContext { get; } + + public InvocationContext(SemanticModel semModel, int position, SyntaxNode receiver, ArgumentListSyntax argList, bool isStatic) + { + SemanticModel = semModel; + Position = position; + Receiver = receiver; + ArgumentTypes = argList.Arguments.Select(argument => semModel.GetTypeInfo(argument.Expression)); + Separators = argList.Arguments.GetSeparators(); + IsInStaticContext = isStatic; + } + + public InvocationContext(SemanticModel semModel, int position, SyntaxNode receiver, AttributeArgumentListSyntax argList, bool isStatic) + { + SemanticModel = semModel; + Position = position; + Receiver = receiver; + ArgumentTypes = argList.Arguments.Select(argument => semModel.GetTypeInfo(argument.Expression)); + Separators = argList.Arguments.GetSeparators(); + IsInStaticContext = isStatic; + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/Command.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/Command.cs new file mode 100644 index 0000000..b212328 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/Command.cs @@ -0,0 +1,9 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct Command +{ + public string Id { get; set; } + public string Title { get; set; } + public string? Tooltip { get; set; } + public object[]? Arguments { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionExtensions.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionExtensions.cs new file mode 100644 index 0000000..3b0526f --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionExtensions.cs @@ -0,0 +1,248 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Tags; +using Microsoft.CodeAnalysis.Text; +using RobotNet.WebApp.Scripts.Monaco.Editor; +using System.Collections.Immutable; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public static class CompletionExtensions +{ + private static readonly Dictionary s_roslynTagToCompletionItemKind = new() + { + { WellKnownTags.Public, CompletionItemKind.Keyword }, + { WellKnownTags.Protected, CompletionItemKind.Keyword }, + { WellKnownTags.Private, CompletionItemKind.Keyword }, + { WellKnownTags.Internal, CompletionItemKind.Keyword }, + { WellKnownTags.File, CompletionItemKind.File }, + { WellKnownTags.Project, CompletionItemKind.File }, + { WellKnownTags.Folder, CompletionItemKind.Folder }, + { WellKnownTags.Assembly, CompletionItemKind.File }, + { WellKnownTags.Class, CompletionItemKind.Class }, + { WellKnownTags.Constant, CompletionItemKind.Constant }, + { WellKnownTags.Delegate, CompletionItemKind.Function }, + { WellKnownTags.Enum, CompletionItemKind.Enum }, + { WellKnownTags.EnumMember, CompletionItemKind.EnumMember }, + { WellKnownTags.Event, CompletionItemKind.Event }, + { WellKnownTags.ExtensionMethod, CompletionItemKind.Method }, + { WellKnownTags.Field, CompletionItemKind.Field }, + { WellKnownTags.Interface, CompletionItemKind.Interface }, + { WellKnownTags.Intrinsic, CompletionItemKind.Text }, + { WellKnownTags.Keyword, CompletionItemKind.Keyword }, + { WellKnownTags.Label, CompletionItemKind.Text }, + { WellKnownTags.Local, CompletionItemKind.Variable }, + { WellKnownTags.Namespace, CompletionItemKind.Module }, + { WellKnownTags.Method, CompletionItemKind.Method }, + { WellKnownTags.Module, CompletionItemKind.Module }, + { WellKnownTags.Operator, CompletionItemKind.Operator }, + { WellKnownTags.Parameter, CompletionItemKind.Value }, + { WellKnownTags.Property, CompletionItemKind.Property }, + { WellKnownTags.RangeVariable, CompletionItemKind.Variable }, + { WellKnownTags.Reference, CompletionItemKind.Reference }, + { WellKnownTags.Structure, CompletionItemKind.Struct }, + { WellKnownTags.TypeParameter, CompletionItemKind.TypeParameter }, + { WellKnownTags.Snippet, CompletionItemKind.Snippet }, + { WellKnownTags.Error, CompletionItemKind.Text }, + { WellKnownTags.Warning, CompletionItemKind.Text }, + }; + + private static CompletionTrigger GetCompletionTrigger(int kind, char? triggerCharacter, bool includeTriggerCharacter) + => kind switch + { + 1 => CompletionTrigger.Invoke, + // https://github.com/dotnet/roslyn/issues/42982: Passing a trigger character + // to GetCompletionsAsync causes a null ref currently. + 2 when includeTriggerCharacter => CompletionTrigger.CreateInsertionTrigger((char)triggerCharacter!), + _ => CompletionTrigger.Invoke, + }; + + //private static readonly Regex EscapeRegex = new(@"([\\\$}])", RegexOptions.Compiled); + //private static string GetAdjustedInsertTextWithPosition( + // CompletionChange change, + // int originalPosition, + // int newOffset, + // string? prependText = null) + //{ + // string newText = change.TextChange.NewText!; + // if (change.NewPosition is not int newPosition + // || newPosition >= change.TextChange.Span.Start + newText.Length) + // { + // return prependText + newText[newOffset..]; + // } + // int midpoint = newPosition - change.TextChange.Span.Start; + + // var beforeText = prependText + newText[newOffset..midpoint]; + // if (beforeText != null) beforeText = EscapeRegex.Replace(beforeText, @"\$1"); + + // var afterText = newText[midpoint..]; + // if (afterText != null) afterText = EscapeRegex.Replace(afterText, @"\$1"); + + // return beforeText + "$0" + afterText; + //} + + private static ImmutableArray BuildCommitCharacters(Microsoft.CodeAnalysis.Completion.CompletionList completions, ImmutableArray characterRules, ImmutableArray.Builder triggerCharactersBuilder) + { + if (completions is null) return []; + + triggerCharactersBuilder.AddRange(completions.Rules.DefaultCommitCharacters); + + foreach (var modifiedRule in characterRules) + { + switch (modifiedRule.Kind) + { + case CharacterSetModificationKind.Add: + triggerCharactersBuilder.AddRange(modifiedRule.Characters); + break; + + case CharacterSetModificationKind.Remove: + for (int i = 0; i < triggerCharactersBuilder.Count; i++) + { + if (modifiedRule.Characters.Contains(triggerCharactersBuilder[i])) + { + triggerCharactersBuilder.RemoveAt(i); + i--; + } + } + + break; + + case CharacterSetModificationKind.Replace: + triggerCharactersBuilder.Clear(); + triggerCharactersBuilder.AddRange(modifiedRule.Characters); + break; + } + } + + if (completions.SuggestionModeItem is not null) + { + triggerCharactersBuilder.Remove(' '); + } + + if (triggerCharactersBuilder.Capacity == triggerCharactersBuilder.Count) + { + return triggerCharactersBuilder.MoveToImmutable(); + } + else + { + var result = triggerCharactersBuilder.ToImmutable(); + triggerCharactersBuilder.Clear(); + return result; + } + } + + private static CompletionItemKind GetCompletionItemKind(ImmutableArray tags) + { + foreach (var tag in tags) + { + if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind)) + { + return itemKind; + } + } + + return CompletionItemKind.Text; + } + + public static async Task> GetCompletionAsync(this AdhocWorkspace workspace, DocumentId documentId, int line, int column, int kind, char? triggerCharacter) + { + if (triggerCharacter == ' ') return []; + + var document = workspace.CurrentSolution.GetDocument(documentId); + if (document is null) return []; + + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines.GetPosition(new LinePosition(line - 1, column - 1)); + var completionService = CompletionService.GetService(document); + + if (completionService == null) return []; + + if (kind == 3 && !completionService.ShouldTriggerCompletion(sourceText, position, GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: true))) + { + return []; + } + + Microsoft.CodeAnalysis.Completion.CompletionList? completionList = null; + try + { + completionList = await completionService.GetCompletionsAsync( + document, + position, + GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: false)); + } + catch (Exception ex) + { + // Log the exception or handle it as needed + Console.WriteLine($"Error getting completions: position:{position} \n {ex}"); + } + + if (completionList is null || completionList.ItemsList.Count <= 0) return []; + + var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position); + string typedText = sourceText.GetSubText(typedSpan).ToString(); + ImmutableArray filteredItems = typedText != string.Empty + ? [.. completionService.FilterItems(document, [.. completionList.ItemsList], typedText).Select(i => i.DisplayText)] + : []; + + bool expectingImportedItems = workspace.Options.GetOption(new PerLanguageOption("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null), LanguageNames.CSharp) == true; + var syntax = await document.GetSyntaxTreeAsync(); + var replacingSpanStartPosition = sourceText.Lines.GetLinePosition(typedSpan.Start); + var replacingSpanEndPosition = sourceText.Lines.GetLinePosition(typedSpan.End); + var triggerCharactersBuilder = ImmutableArray.CreateBuilder(completionList.Rules.DefaultCommitCharacters.Length); + var completionsBuilder = new List(); + + foreach (var completion in completionList.ItemsList) + { + SingleEditOperation[]? additionalTextEdits = null; + char sortTextPrepend = '0'; + + if (!completion.Properties.TryGetValue("InsertionText", out string? insertText)) + { + if (GetCompletionItemKind(completion.Tags) == CompletionItemKind.Keyword) + { + insertText = completion.DisplayText; + } + else + { + continue; + } + } + + if (string.IsNullOrEmpty(insertText)) continue; + + var commitCharacters = BuildCommitCharacters(completionList, completion.Rules.CommitCharacterRules, triggerCharactersBuilder).Select(c => c.ToString()); + + completionsBuilder.Add(new CompletionItem + { + Label = completion.DisplayTextPrefix + completion.DisplayText + completion.DisplayTextSuffix, + Kind = GetCompletionItemKind(completion.Tags), + Documentation = "", + InsertText = insertText, + Range = new Range + { + StartLineNumber = replacingSpanStartPosition.Line + 1, + EndLineNumber = replacingSpanEndPosition.Line + 1, + StartColumn = replacingSpanStartPosition.Character + 1, + EndColumn = replacingSpanEndPosition.Character + 1, + }, + AdditionalTextEdits = additionalTextEdits, + // Ensure that unimported items are sorted after things already imported. + SortText = expectingImportedItems ? sortTextPrepend + completion.SortText : completion.SortText, + FilterText = completion.FilterText, + Detail = completion.InlineDescription, + Preselect = completion.Rules.MatchPriority == MatchPriority.Preselect || filteredItems.Contains(completion.DisplayText), + CommitCharacters = [.. commitCharacters], + Tags = null, + Command = null, + InsertTextRules = null, + }); + } + + return completionsBuilder; + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItem.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItem.cs new file mode 100644 index 0000000..200b8ce --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItem.cs @@ -0,0 +1,21 @@ +using RobotNet.WebApp.Scripts.Monaco.Editor; + +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct CompletionItem +{ + public string Label { get; set; } + public CompletionItemKind Kind { get; set; } + public string? Documentation { get; set; } + public string InsertText { get; set; } + public Range Range { get; set; } + public SingleEditOperation[]? AdditionalTextEdits { get; set; } + public Command? Command { get; set; } + public string[]? CommitCharacters { get; set; } + public string? Detail { get; set; } + public string? FilterText { get; set; } + public CompletionItemInsertTextRule? InsertTextRules { get; set; } + public bool Preselect { get; set; } + public string? SortText { get; set; } + public int[]? Tags { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemInsertTextRule.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemInsertTextRule.cs new file mode 100644 index 0000000..1e7bbc6 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemInsertTextRule.cs @@ -0,0 +1,15 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public enum CompletionItemInsertTextRule +{ + None = 0, + /** + * Adjust whitespace/indentation of multiline insert texts to + * match the current line indentation. + */ + KeepWhitespace = 1, + /** + * `insertText` is a snippet. + */ + InsertAsSnippet = 4 +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemKind.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemKind.cs new file mode 100644 index 0000000..358eb2e --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemKind.cs @@ -0,0 +1,33 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public enum CompletionItemKind +{ + Method = 0, + Function = 1, + Constructor = 2, + Field = 3, + Variable = 4, + Class = 5, + Struct = 6, + Interface = 7, + Module = 8, + Property = 9, + Event = 10, + Operator = 11, + Unit = 12, + Value = 13, + Constant = 14, + Enum = 15, + EnumMember = 16, + Keyword = 17, + Text = 18, + Color = 19, + File = 20, + Reference = 21, + Customcolor = 22, + Folder = 23, + TypeParameter = 24, + User = 25, + Issue = 26, + Snippet = 27 +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemLabel.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemLabel.cs new file mode 100644 index 0000000..f40c39c --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemLabel.cs @@ -0,0 +1,8 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct CompletionItemLabel +{ + public string? Description { get; set; } + public string? Detail { get; set; } + public string Label { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemRanges.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemRanges.cs new file mode 100644 index 0000000..8f33493 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemRanges.cs @@ -0,0 +1,9 @@ +using RobotNet.WebApp.Scripts.Monaco; + +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct CompletionItemRanges +{ + public Range Intert { get; set; } + public Range Replace { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemTag.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemTag.cs new file mode 100644 index 0000000..dd2653b --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionItemTag.cs @@ -0,0 +1,6 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public enum CompletionItemTag +{ + Deprecated = 1 +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionList.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionList.cs new file mode 100644 index 0000000..6904aa2 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionList.cs @@ -0,0 +1,7 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct CompletionList +{ + public bool? Incomplete { get; set; } + public CompletionItem[] Suggestions { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/ParameterInformation.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/ParameterInformation.cs new file mode 100644 index 0000000..aa8f7e6 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/ParameterInformation.cs @@ -0,0 +1,7 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct ParameterInformation +{ + public string Label { get; set; } + public MarkdownString? Documentation { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelp.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelp.cs new file mode 100644 index 0000000..4442e0a --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelp.cs @@ -0,0 +1,8 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct SignatureHelp +{ + public int ActiveParameter { get; set; } + public int ActiveSignature { get; set; } + public SignatureInformation[] Signatures { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpExtensions.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpExtensions.cs new file mode 100644 index 0000000..fc1eacc --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpExtensions.cs @@ -0,0 +1,169 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using RobotNet.WebApp.Scripts.Monaco.Documentation; + +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public static class SignatureHelpExtensions +{ + public static async Task GetSignatureHelpAsync(this Document document, int line, int column) + { + var invocation = await GetInvocation(document, line - 1, column - 1); + if (invocation is null) return null; + + var response = new SignatureHelp(); + foreach (var comma in invocation.Separators) + { + if (comma.Span.Start > invocation.Position) + { + break; + } + response.ActiveParameter += 1; + } + + var signaturesSet = new HashSet(); + var bestScore = int.MinValue; + SignatureInformation? bestScoredItem = null; + + var types = invocation.ArgumentTypes; + ISymbol? throughSymbol = null; + ISymbol? throughType = null; + var methodGroup = invocation.SemanticModel.GetMemberGroup(invocation.Receiver).OfType(); + if (invocation.Receiver is MemberAccessExpressionSyntax syntax) + { + var throughExpression = syntax.Expression; + throughSymbol = invocation.SemanticModel.GetSpeculativeSymbolInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsExpression).Symbol; + throughType = invocation.SemanticModel.GetSpeculativeTypeInfo(invocation.Position, throughExpression, SpeculativeBindingOption.BindAsTypeOrNamespace).Type; + var includeInstance = throughSymbol != null && throughSymbol is not ITypeSymbol || + throughExpression is LiteralExpressionSyntax || + throughExpression is TypeOfExpressionSyntax; + var includeStatic = throughSymbol is INamedTypeSymbol || throughType != null; + methodGroup = methodGroup.Where(m => m.IsStatic && includeStatic || !m.IsStatic && includeInstance); + } + else if (invocation.Receiver is SimpleNameSyntax && invocation.IsInStaticContext) + { + methodGroup = methodGroup.Where(m => m.IsStatic || m.MethodKind == MethodKind.LocalFunction); + } + + foreach (var methodOverload in methodGroup) + { + var signature = BuildSignature(methodOverload); + signaturesSet.Add(signature); + + var score = InvocationScore(methodOverload, types); + if (score > bestScore) + { + bestScore = score; + bestScoredItem = signature; + } + } + + var signaturesList = signaturesSet.ToList(); + response.Signatures = [.. signaturesList]; + if(bestScoredItem == null) + { + response.ActiveSignature = -1; + } + else + { + response.ActiveSignature = signaturesList.IndexOf((SignatureInformation)bestScoredItem); + } + + return new SignatureHelpResult() + { + Value = response, + }; + } + + private static async Task GetInvocation(Document document, int line, int column) + { + var sourceText = await document.GetTextAsync(); + var position = sourceText.Lines.GetPosition(new LinePosition(line, column)); + var tree = await document.GetSyntaxTreeAsync(); + + if (tree is null) return null; + + var root = await tree.GetRootAsync(); + var node = root.FindToken(position).Parent; + + // Walk up until we find a node that we're interested in. + while (node != null) + { + if (node is InvocationExpressionSyntax invocation && invocation.ArgumentList.Span.Contains(position)) + { + var semanticModel = await document.GetSemanticModelAsync(); + return semanticModel is null ? null : new InvocationContext(semanticModel, position, invocation.Expression, invocation.ArgumentList, invocation.IsInStaticContext()); + } + + if (node is BaseObjectCreationExpressionSyntax objectCreation && (objectCreation.ArgumentList?.Span.Contains(position) ?? false)) + { + var semanticModel = await document.GetSemanticModelAsync(); + return semanticModel is null ? null : new InvocationContext(semanticModel, position, objectCreation, objectCreation.ArgumentList, objectCreation.IsInStaticContext()); + } + + if (node is AttributeSyntax attributeSyntax && (attributeSyntax.ArgumentList?.Span.Contains(position) ?? false)) + { + var semanticModel = await document.GetSemanticModelAsync(); + return semanticModel is null ? null : new InvocationContext(semanticModel, position, attributeSyntax, attributeSyntax.ArgumentList, attributeSyntax.IsInStaticContext()); + } + + node = node.Parent; + } + + return null; + } + + private static int InvocationScore(IMethodSymbol symbol, IEnumerable types) + { + var parameters = symbol.Parameters; + if (parameters.Length < types.Count()) + { + return int.MinValue; + } + + var score = 0; + var invocationEnum = types.GetEnumerator(); + var definitionEnum = parameters.GetEnumerator(); + while (invocationEnum.MoveNext() && definitionEnum.MoveNext()) + { + if (invocationEnum.Current.ConvertedType == null) + { + // 1 point for having a parameter + score += 1; + } + else if (SymbolEqualityComparer.Default.Equals(invocationEnum.Current.ConvertedType, definitionEnum.Current.Type)) + { + // 2 points for having a parameter and being + // the same type + score += 2; + } + } + + return score; + } + + private static SignatureInformation BuildSignature(IMethodSymbol symbol) + { + var StructuredDocumentation = DocumentationConverter.GetStructuredDocumentation(symbol); + + return new SignatureInformation + { + Documentation = new MarkdownString() + { + Value = StructuredDocumentation?.SummaryText ?? "", + }, + Label = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + Parameters = [..symbol.Parameters.Select(parameter => new ParameterInformation() + { + Label = parameter.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + Documentation = new MarkdownString() + { + Value = StructuredDocumentation?.GetParameterText(parameter.Name) ?? string.Empty, + }, + })], + ActiveParameter = null, + }; + } + +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpResult.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpResult.cs new file mode 100644 index 0000000..9da6c1e --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureHelpResult.cs @@ -0,0 +1,6 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct SignatureHelpResult +{ + public SignatureHelp Value { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureInformation.cs b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureInformation.cs new file mode 100644 index 0000000..af01fea --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Languages/SignatureInformation.cs @@ -0,0 +1,9 @@ +namespace RobotNet.WebApp.Scripts.Monaco.Languages; + +public struct SignatureInformation +{ + public int? ActiveParameter { get; set; } + public MarkdownString? Documentation { get; set; } + public string Label { get; set; } + public ParameterInformation[] Parameters { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/MarkdownHelpers.cs b/RobotNet.WebApp/Scripts/Monaco/MarkdownHelpers.cs new file mode 100644 index 0000000..7f01ac8 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/MarkdownHelpers.cs @@ -0,0 +1,260 @@ +using Microsoft.CodeAnalysis; +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using System.Text; + +namespace RobotNet.WebApp.Scripts.Monaco; + +public enum MarkdownFormat +{ + Default, + Italicize, + FirstLineAsCSharp, + FirstLineDefaultRestCSharp, + AllTextAsCSharp +} + +public static class MarkdownHelpers +{ + private static readonly Regex EscapeRegex = new("([\\\\`\\*_\\{\\}\\[\\]\\(\\)#+\\-\\.!])", RegexOptions.Compiled); + + private const string ContainerStart = "ContainerStart"; + + private const string ContainerEnd = "ContainerEnd"; + + public static string Escape(string markdown) => string.IsNullOrEmpty(markdown) ? string.Empty : EscapeRegex.Replace(markdown, "\\$1"); + + public static void TaggedTextToMarkdown(ImmutableArray taggedParts, StringBuilder stringBuilder, string newLine, MarkdownFormat markdownFormat, out bool endedWithLineBreak) + { + bool isInCodeBlock = false; + bool brokeLine = true; + bool afterFirstLine = false; + if (markdownFormat == MarkdownFormat.Italicize) + { + stringBuilder.Append('_'); + } + + int num = 0; + while (num < taggedParts.Length) + { + TaggedText taggedText = taggedParts[num]; + bool flag; + if (brokeLine && markdownFormat != MarkdownFormat.Italicize) + { + brokeLine = false; + if (!afterFirstLine) + { + if (markdownFormat != MarkdownFormat.FirstLineAsCSharp) + { + goto IL_00a2; + } + + flag = true; + } + else + { + if (markdownFormat != MarkdownFormat.FirstLineDefaultRestCSharp) + { + goto IL_00a2; + } + + flag = true; + } + + goto IL_00bf; + } + + goto IL_0279; + IL_00a2: + flag = markdownFormat == MarkdownFormat.AllTextAsCSharp; + goto IL_00bf; + IL_0279: + switch (taggedText.Tag) + { + case "Text": + if (!isInCodeBlock) + { + addText(taggedText.Text); + break; + } + + endBlock(); + addText(taggedText.Text); + break; + case "Space": + if (isInCodeBlock) + { + if (indexIsTag(num + 1, ["Text"])) + { + endBlock(); + } + + addText(taggedText.Text); + break; + } + + goto case "Punctuation"; + case "Punctuation": + addText(taggedText.Text); + break; + case ContainerStart: + addNewline(); + addText(taggedText.Text); + break; + case ContainerEnd: + addNewline(); + break; + case "LineBreak": + if (stringBuilder.Length != 0 && !indexIsTag(num + 1, [ContainerStart, ContainerEnd]) && num + 1 != taggedParts.Length) + { + addNewline(); + } + + break; + default: + if (!isInCodeBlock) + { + isInCodeBlock = true; + stringBuilder.Append('`'); + } + + stringBuilder.Append(taggedText.Text); + brokeLine = false; + break; + } + + num++; + continue; + IL_00bf: + bool flag2 = flag; + if (!flag2) + { + for (int j = num; j < taggedParts.Length; flag2 = true, j++) + { + switch (taggedParts[j].Tag) + { + case "Text": + flag2 = false; + break; + default: + continue; + case ContainerStart: + case ContainerEnd: + case "LineBreak": + break; + } + + break; + } + } + else + { + flag2 = !indexIsTag(num, + [ + ContainerStart, + ContainerEnd, + "LineBreak" + ]); + } + + if (flag2) + { + afterFirstLine = true; + stringBuilder.Append("```csharp"); + stringBuilder.Append(newLine); + while (true) + { + if (num < taggedParts.Length) + { + taggedText = taggedParts[num]; + if (taggedText.Tag == ContainerStart || taggedText.Tag == ContainerEnd || taggedText.Tag == "LineBreak") + { + stringBuilder.Append(newLine); + if (markdownFormat != MarkdownFormat.AllTextAsCSharp && markdownFormat != MarkdownFormat.FirstLineDefaultRestCSharp) + { + break; + } + } + else + { + stringBuilder.Append(taggedText.Text); + } + + num++; + continue; + } + + stringBuilder.Append(newLine); + stringBuilder.Append("```"); + endedWithLineBreak = false; + return; + } + + stringBuilder.Append("```"); + } + + goto IL_0279; + } + + if (isInCodeBlock) + { + endBlock(); + } + + if (!brokeLine && markdownFormat == MarkdownFormat.Italicize) + { + stringBuilder.Append('_'); + } + + endedWithLineBreak = brokeLine; + void addNewline() + { + if (isInCodeBlock) + { + endBlock(); + } + + if (markdownFormat == MarkdownFormat.Italicize) + { + stringBuilder.Append('_'); + } + + stringBuilder.Append(newLine); + stringBuilder.Append(newLine); + brokeLine = true; + if (markdownFormat == MarkdownFormat.Italicize) + { + stringBuilder.Append('_'); + } + } + + void addText(string text) + { + brokeLine = false; + afterFirstLine = true; + if (!isInCodeBlock) + { + text = Escape(text); + } + + stringBuilder.Append(text); + } + + void endBlock() + { + stringBuilder.Append('`'); + isInCodeBlock = false; + } + + bool indexIsTag(int i, string[] tags) + { + if (i < taggedParts.Length) + { + return tags.Contains(taggedParts[i].Tag); + } + + return false; + } + } +} + diff --git a/RobotNet.WebApp/Scripts/Monaco/MarkdownString.cs b/RobotNet.WebApp/Scripts/Monaco/MarkdownString.cs new file mode 100644 index 0000000..700b011 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/MarkdownString.cs @@ -0,0 +1,11 @@ +namespace RobotNet.WebApp.Scripts.Monaco; + +public struct MarkdownString +{ + public string Value { get; set; } + public bool? IsTrusted { get; set; } // TODO | MarkdownStringTrustedOptions; + public bool? SupportThemeIcons { get; set; } + public bool? SupportHtml { get; set; } + public UriComponents? BaseUri { get; set; } + public IDictionary? Uris { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/MarkerSeverity.cs b/RobotNet.WebApp/Scripts/Monaco/MarkerSeverity.cs new file mode 100644 index 0000000..3a28210 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/MarkerSeverity.cs @@ -0,0 +1,9 @@ +namespace RobotNet.WebApp.Scripts.Monaco; + +public enum MarkerSeverity +{ + Hint = 1, + Info = 2, + Warning = 4, + Error = 8, +} diff --git a/RobotNet.WebApp/Scripts/Monaco/MarkerTag.cs b/RobotNet.WebApp/Scripts/Monaco/MarkerTag.cs new file mode 100644 index 0000000..196140d --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/MarkerTag.cs @@ -0,0 +1,7 @@ +namespace RobotNet.WebApp.Scripts.Monaco; + +public enum MarkerTag +{ + Unnecessary = 1, + Deprecated = 2, +} diff --git a/RobotNet.WebApp/Scripts/Monaco/MonacoExtensions.cs b/RobotNet.WebApp/Scripts/Monaco/MonacoExtensions.cs new file mode 100644 index 0000000..4678d75 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/MonacoExtensions.cs @@ -0,0 +1,150 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace RobotNet.WebApp.Scripts.Monaco; + +public static class MonacoExtensions +{ + public static bool IsInStaticContext(this SyntaxNode node) + { + // this/base calls are always static. + if (node.FirstAncestorOrSelf() != null) + { + return true; + } + + var memberDeclaration = node.FirstAncestorOrSelf(); + if (memberDeclaration == null) + { + return false; + } + + switch (memberDeclaration.Kind()) + { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.ConstructorDeclaration: + case SyntaxKind.EventDeclaration: + case SyntaxKind.IndexerDeclaration: + return GetModifiers(memberDeclaration).Any(SyntaxKind.StaticKeyword); + + case SyntaxKind.PropertyDeclaration: + return GetModifiers(memberDeclaration).Any(SyntaxKind.StaticKeyword) || + node.IsFoundUnder((PropertyDeclarationSyntax p) => p.Initializer); + + case SyntaxKind.FieldDeclaration: + case SyntaxKind.EventFieldDeclaration: + // Inside a field one can only access static members of a type (unless it's top-level). + return !memberDeclaration.Parent.IsKind(SyntaxKind.CompilationUnit); + + case SyntaxKind.DestructorDeclaration: + return false; + } + + // Global statements are not a static context. + if (node.FirstAncestorOrSelf() != null) + { + return false; + } + + // any other location is considered static + return true; + } + + public static SyntaxTokenList GetModifiers(SyntaxNode member) + { + if (member != null) + { + switch (member.Kind()) + { + case SyntaxKind.EnumDeclaration: + return ((EnumDeclarationSyntax)member).Modifiers; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.StructDeclaration: + return ((TypeDeclarationSyntax)member).Modifiers; + case SyntaxKind.DelegateDeclaration: + return ((DelegateDeclarationSyntax)member).Modifiers; + case SyntaxKind.FieldDeclaration: + return ((FieldDeclarationSyntax)member).Modifiers; + case SyntaxKind.EventFieldDeclaration: + return ((EventFieldDeclarationSyntax)member).Modifiers; + case SyntaxKind.ConstructorDeclaration: + return ((ConstructorDeclarationSyntax)member).Modifiers; + case SyntaxKind.DestructorDeclaration: + return ((DestructorDeclarationSyntax)member).Modifiers; + case SyntaxKind.PropertyDeclaration: + return ((PropertyDeclarationSyntax)member).Modifiers; + case SyntaxKind.EventDeclaration: + return ((EventDeclarationSyntax)member).Modifiers; + case SyntaxKind.IndexerDeclaration: + return ((IndexerDeclarationSyntax)member).Modifiers; + case SyntaxKind.OperatorDeclaration: + return ((OperatorDeclarationSyntax)member).Modifiers; + case SyntaxKind.ConversionOperatorDeclaration: + return ((ConversionOperatorDeclarationSyntax)member).Modifiers; + case SyntaxKind.MethodDeclaration: + return ((MethodDeclarationSyntax)member).Modifiers; + case SyntaxKind.GetAccessorDeclaration: + case SyntaxKind.SetAccessorDeclaration: + case SyntaxKind.AddAccessorDeclaration: + case SyntaxKind.RemoveAccessorDeclaration: + return ((AccessorDeclarationSyntax)member).Modifiers; + } + } + + return default; + } + + public static bool IsFoundUnder(this SyntaxNode node, Func childGetter) + where TParent : SyntaxNode + { + var ancestor = node.GetAncestor(); + if (ancestor == null) + { + return false; + } + + var child = childGetter(ancestor); + + // See if node passes through child on the way up to ancestor. + return node.GetAncestorsOrThis().Contains(child); + } + + public static TNode? GetAncestor(this SyntaxNode node) + where TNode : SyntaxNode + { + var current = node.Parent; + while (current != null) + { + if (current is TNode tNode) + { + return tNode; + } + + current = current.GetParent(); + } + + return null; + } + + public static IEnumerable GetAncestorsOrThis(this SyntaxNode node) + where TNode : SyntaxNode + { + var current = node; + while (current != null) + { + if (current is TNode tNode) + { + yield return tNode; + } + + current = current.GetParent(); + } + } + + private static SyntaxNode? GetParent(this SyntaxNode node) + { + return node is IStructuredTriviaSyntax trivia ? trivia.ParentTrivia.Token.Parent : node.Parent; + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/Range.cs b/RobotNet.WebApp/Scripts/Monaco/Range.cs new file mode 100644 index 0000000..25118af --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/Range.cs @@ -0,0 +1,9 @@ +namespace RobotNet.WebApp.Scripts.Monaco; + +public struct Range +{ + public int EndColumn { get; set; } + public int EndLineNumber { get; set; } + public int StartColumn { get; set; } + public int StartLineNumber { get; set; } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/StringBuilderExtension.cs b/RobotNet.WebApp/Scripts/Monaco/StringBuilderExtension.cs new file mode 100644 index 0000000..84daa5c --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/StringBuilderExtension.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis.QuickInfo; +using System.Text; + +namespace RobotNet.WebApp.Scripts.Monaco; + +public static class StringBuilderExtension +{ + public static void AppendSection(this StringBuilder builder, QuickInfoSection section, MarkdownFormat format, ref bool lastLineBreak) + { + if (!lastLineBreak && section.TaggedParts.Length > 0 && section.TaggedParts[0].Tag != "LineBreak") + { + builder.Append("\n\n"); + } + MarkdownHelpers.TaggedTextToMarkdown(section.TaggedParts, builder, "\n", format, out lastLineBreak); + } +} diff --git a/RobotNet.WebApp/Scripts/Monaco/UriComponents.cs b/RobotNet.WebApp/Scripts/Monaco/UriComponents.cs new file mode 100644 index 0000000..67b2177 --- /dev/null +++ b/RobotNet.WebApp/Scripts/Monaco/UriComponents.cs @@ -0,0 +1,10 @@ +namespace RobotNet.WebApp.Scripts.Monaco; + +public struct UriComponents +{ + public string? Scheme { get; set; } + public string? Authority { get; set; } + public string? Path { get; set; } + public string? Query { get; set; } + public string? Fragment { get; set; } +} diff --git a/RobotNet.WebApp/_Imports.razor b/RobotNet.WebApp/_Imports.razor new file mode 100644 index 0000000..92372bc --- /dev/null +++ b/RobotNet.WebApp/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop +@using MudBlazor +@using RobotNet.WebApp +@using RobotNet.WebApp.Components diff --git a/RobotNet.WebApp/libman.json b/RobotNet.WebApp/libman.json new file mode 100644 index 0000000..c374e26 --- /dev/null +++ b/RobotNet.WebApp/libman.json @@ -0,0 +1,19 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [ + { + "library": "bootstrap@5.3.7", + "destination": "wwwroot/lib/bootstrap/" + }, + { + "provider": "jsdelivr", + "library": "@mdi/font@7.4.47", + "destination": "wwwroot/lib/mdi/font/" + }, + { + "library": "monaco-editor@0.52.2", + "destination": "wwwroot/lib/monaco-editor/" + } + ] +} \ No newline at end of file diff --git a/RobotNet.WebApp/nginx.conf b/RobotNet.WebApp/nginx.conf new file mode 100644 index 0000000..3d0ce01 --- /dev/null +++ b/RobotNet.WebApp/nginx.conf @@ -0,0 +1,52 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log notice; + +include /usr/share/nginx/modules/*.conf; +events { + worker_connections 1024; + } +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # sendfile on; + # keepalive_timeout 65; + + types { + # application/wasm wasm; + # application/octet-stream dll; + # application/manifest+json webmanifest; + } + server { + listen 80; + listen [::]:80; + server_name localhost; + return 301 https://$host$request_uri; # Chuyển hướng từ HTTP sang HTTPS + } + + server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/cert.pem; + ssl_certificate_key /etc/nginx/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + root /usr/share/nginx/html; + index index.html; + location / { + try_files $uri $uri/ /index.html; + } + + error_page 404 /index.html; # Đảm bảo các lỗi 404 cũng phục vụ index.html + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/appsettings.json b/RobotNet.WebApp/wwwroot/appsettings.json new file mode 100644 index 0000000..217c565 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/appsettings.json @@ -0,0 +1,48 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "Local": { + "Authority": "https://localhost:7061", + "ClientId": "robotnet-webapp", + "ResponseType": "code", + "DefaultScopes": [ + "openid", + "profile", + "email", + "roles", + "robotnet-script-api", + "robotnet-robot-api", + "robotnet-map-api" + ], + "RedirectUri": "https://localhost:7035/authentication/login-callback", + "PostLogoutRedirectUri": "https://localhost:7035/authentication/logout-callback" + }, + "ScriptManager": { + "BaseAddress": "https://localhost:7102" + }, + "RobotManager": { + "BaseAddress": "https://localhost:7179" + }, + "MapManager": { + "BaseAddress": "https://localhost:7177" + }, + "Logs": [ + { + "Name": "RobotManager", + "Url": "https://localhost:7179/api/RobotManagerLogger" + }, + { + "Name": "MapManager", + "Url": "https://localhost:7177/api/MapDesignerLogger" + }, + { + "Name": "ScriptManager", + "Url": "https://localhost:7102/api/ScriptManagerLogger" + } + ] +} diff --git a/RobotNet.WebApp/wwwroot/css/app.css b/RobotNet.WebApp/wwwroot/css/app.css new file mode 100644 index 0000000..74544f5 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/css/app.css @@ -0,0 +1,147 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + width: 100vw; + height: 100vh; +} + +#app { + width: 100vw; + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: row; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid red; +} + +.validation-message { + color: red; +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.loading-progress { + position: relative; + display: block; + width: 8rem; + height: 8rem; + margin: 20vh auto 1rem auto; +} + + .loading-progress circle { + fill: none; + stroke: #e0e0e0; + stroke-width: 0.6rem; + transform-origin: 50% 50%; + transform: rotate(-90deg); + } + + .loading-progress circle:last-child { + stroke: #1b6ec2; + stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; + } + +.loading-progress-text { + position: absolute; + text-align: center; + font-weight: bold; + inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +} + + .loading-progress-text:after { + content: var(--blazor-load-percentage-text, "Loading"); + } + +code { + color: #c02d76; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} + +.mdi-btn-medium { + width: 28px; + height: 28px; + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; +} + + .mdi-btn-medium > span.mdi { + font-size: 30px; + } + +main { + flex-grow: 1; + height: 100%; + position: relative; + overflow: hidden; +} + + main > div { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + overflow: hidden; + } + + +:root { + --dashboard-card-background-color: linear-gradient(0deg, #404040, #303030); + --dashboard-text-common-color: #29a3ff; + --dashboard-text-green-color: #00cc00; + --dashboard-text-red-color: #ff0000; + --dashboard-text-orange-color: #ff6600; + --dashboard-text-bluesky-color: #29a3ff; + --dashboard-text-font-family: 'Roboto', sans-serif; + --dashboard-text-white-color: #d6d6db; + --edge-stroke-width: 0.15px; + --edge-direction-stroke-width: 0.3px; + --node-r: 0.1px; + --origin-vector-stroke-width: 0.35px; +} diff --git a/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 b/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmQiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b0ed6d697b45af5d168e2095a7f9d42ab5037983 GIT binary patch literal 11840 zcmV-GF2B)tPew8T0RR9104_iP5&!@I09*6`04>@80RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 b/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmXiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..de646f8d4fbfcc314053710c299fcc60716999b0 GIT binary patch literal 9644 zcmV;dB~#jWPew8T0RR91041yd5&!@I07Mi303}BN0RR9100000000000000000000 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 Pab&0C?+heW+1Nz@tgQ^J literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 b/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmaiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..47e69cf8aa8ea6c5daeff3acd53343d5519602d3 GIT binary patch literal 13740 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 W$zFi)LqQISL(Z9if!eGx0RRC0y_JFh literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 b/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmbiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..bc95855cdb4d03ee9a9999e8a4152f94dfcaf123 GIT binary patch literal 7856 zcmV;h9#7$SPew8T0RR9103NUa5&!@I073u&03JyI0RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 b/RobotNet.WebApp/wwwroot/css/mud/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVnoiArmlw.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..15e1583a0f7320719a1e47697fb1986aea0a829f GIT binary patch literal 19660 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 + + + + \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/icon-192.png b/RobotNet.WebApp/wwwroot/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..166f56da7612ea74df6a297154c8d281a4f28a14 GIT binary patch literal 2626 zcmV-I3cdA-P)v0A9xRwxP|bki~~&uFk>U z#P+PQh zyZ;-jwXKqnKbb6)@RaxQz@vm={%t~VbaZrdbaZrdbaeEeXj>~BG?&`J0XrqR#sSlO zg~N5iUk*15JibvlR1f^^1czzNKWvoJtc!Sj*G37QXbZ8LeD{Fzxgdv#Q{x}ytfZ5q z+^k#NaEp>zX_8~aSaZ`O%B9C&YLHb(mNtgGD&Kezd5S@&C=n~Uy1NWHM`t07VQP^MopUXki{2^#ryd94>UJMYW|(#4qV`kb7eD)Q=~NN zaVIRi@|TJ!Rni8J=5DOutQ#bEyMVr8*;HU|)MEKmVC+IOiDi9y)vz=rdtAUHW$yjt zrj3B7v(>exU=IrzC<+?AE=2vI;%fafM}#ShGDZx=0Nus5QHKdyb9pw&4>4XCpa-o?P(Gnco1CGX|U> z$f+_tA3+V~<{MU^A%eP!8R*-sD9y<>Jc7A(;aC5hVbs;kX9&Sa$JMG!W_BLFQa*hM zri__C@0i0U1X#?)Y=)>JpvTnY6^s;fu#I}K9u>OldV}m!Ch`d1Vs@v9 zb}w(!TvOmSzmMBa9gYvD4xocL2r0ds6%Hs>Z& z#7#o9PGHDmfG%JQq`O5~dt|MAQN@2wyJw_@``7Giyy(yyk(m8U*kk5$X1^;3$a3}N^Lp6hE5!#8l z#~NYHmKAs6IAe&A;bvM8OochRmXN>`D`{N$%#dZCRxp4-dJ?*3P}}T`tYa3?zz5BA zTu7uE#GsDpZ$~j9q=Zq!LYjLbZPXFILZK4?S)C-zE1(dC2d<7nO4-nSCbV#9E|E1MM|V<9>i4h?WX*r*ul1 z5#k6;po8z=fdMiVVz*h+iaTlz#WOYmU^SX5#97H~B32s-#4wk<1NTN#g?LrYieCu> zF7pbOLR;q2D#Q`^t%QcY06*X-jM+ei7%ZuanUTH#9Y%FBi*Z#22({_}3^=BboIsbg zR0#jJ>9QR8SnmtSS6x($?$}6$x+q)697#m${Z@G6Ujf=6iO^S}7P`q8DkH!IHd4lB zDzwxt3BHsPAcXFFY^Fj}(073>NL_$A%v2sUW(CRutd%{G`5ow?L`XYSO*Qu?x+Gzv zBtR}Y6`XF4xX7)Z04D+fH;TMapdQFFameUuHL34NN)r@aF4RO%x&NApeWGtr#mG~M z6sEIZS;Uj1HB1*0hh=O@0q1=Ia@L>-tETu-3n(op+97E z#&~2xggrl(LA|giII;RwBlX2^Q`B{_t}gxNL;iB11gEPC>v` zb4SJ;;BFOB!{chn>?cCeGDKuqI0+!skyWTn*k!WiPNBf=8rn;@y%( znhq%8fj2eAe?`A5mP;TE&iLEmQ^xV%-kmC-8mWao&EUK_^=GW-Y3z ksi~={si~={skwfB0gq6itke#r1ONa407*qoM6N<$g11Kq@c;k- literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/icon-512.png b/RobotNet.WebApp/wwwroot/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c2dd4842dc93df73c322218ee03eca142a19a338 GIT binary patch literal 6311 zcmV;Y7+B|tP)SXufPd5Ix6kc7K;%njMy_o-9lc5dH(-`j+Q5JCtcgb+dqA%qY@2qAtDg>+@U|JPy00FBJiUlTQ zK|ch>Gwc9k#$pXdTjMG;8I=Y75I~W81{l;4uOKqU-2vuw#I8bQB5h#6DK|)WHHFA1 zrE&=^CWT>7acWr^rvb2IbpUeUqG^H8hmT$ z?=EK$r04CJ`v+$zr5K&-orRY}#8@*uM;WjH?riq2{|jyUHUs|de)byv3Mc3|7hbQP zBgS~0wbQg4^4E@#tdw>VtlM1p!-IqKy}u1;ya3+UBZX9k&UFF| z3cv;Q!!Pa~AXn4*}u8@O-d7wW7mLlcB-K`>jrQUZZ7ry}h+5z&BJPvyd zhMaB(m;Z5hEp~oX}ZdDmHNmn=Rkw}{H2#KV`J2uT|&rLB5c7+6qO$CWW zESg~7m;|d~fu+P_J+?j#gGl76zW3Z)=Tz7HB7+4ped^wG{^xOT9R(J={|*lnZh-ll zfr%!r55zPmb}a-hS{5m=%8HUc{{N|fYf!WS#(Kbquk_A9fw?BOF9b$jD!n5sWGR=w zTFR;H3!Qe7J9FOxD}mBGa#0iRoM+trs)ipr3I!WrECgmIHQ$}r0AqFFCe6$FS{nc_ zKxl&C&?ay}CZ+uRP}8d)w$8{H6hQJf_7-k{LO#FuH}fdm0Cz&*K@EcEHvtrGfVoWZ zU-g0W_k~cr0fvr1A;|rELV)58FmDr-p2Y3=PWefkrZ;B*4hC2VGl9hA_|8ng+5nXq z-~gQr@CY!}>-Ekwuj};zTR64`m;;ABxEU|9Nn)gByJr5e=ucvf?fadZweLF1i%cwCTQ1$ z_x2B1FFBK#A_BWQm$B!>9$oaSDANZ+^Tgmj$q9O612-4;X2p3ZWS;Kt|#p$~1CNLxuo-04!NB zTp|?Ew^FSae<-6Pthr|aUMjTUyAE8m-P6EG7ws3bDs(6Bg z${XMza$1yxSM)Ce9ukB`ch~^)F7DkkGSWFSGV=Mrb%g4oCq}11_ziIR)4%89<>=>e zMCpZ=)JX)%$lx}N9;nF9fp{Pyfpm+3YqbwQyZ^gkORTNG(-SxigaxtY>GyAcZ`hF* zK858uHA6o1-^7PQ(6FX|5BWQgv(nH7T3GL{QC@#6F4hk&N4P+oihN>v6;7m4pR_D> zaDjeKQ`GM@Z;K~oiM#Z%*(@@koKd;9<6C5c ziHTNlr@^uZ*5PN>Jg!XTqfn?r-n8*9phn{XRD6y-5aC@wmu;FWV%P%-+67cIipB(} z_1a1g@Ro8{bo!x@!JOf zVlL>OiX6SJ3pQU&x9Dxil?Yo9moYvz8(pSzOH&;r(wstr@@zeHWD35zoRVtNCPV@H zs*om9o5;qL(=fiI4TL^g21UsCIv#U8Afh&|WJdm_s<5Xq4^8pc`v`5y) z${=5$9>^~DQ&KXf92JPLKCp(0%O`{uy~=4*W`yXLNQ7CCr-UATR_PN~$33hMR9(P* z$fO~(7zO#5&I$O~33(r}1FEse=Z2~_of4>Ff1GtjyH?e3PDfLHIwjD;wuM_J<73L8 zi{lM7G9^<239=lfhNRu@4nY4Own)+eRS4_*{Z;jW;Wq$&4-6Ca2=PLA*fbB28-*qF zL1hJ1O<*ZvGzOa>T>b!_Z3Qb~#aAc%P6-zlgfptVuC_z39X(|IffkE%$SJL!h zf)u$OJyTISne)XNGS(bmvSTGymkygOSJhDNUJzO&Ua*M`0&PVoA@!B*@wD&7@?t;~nPlZ)*_v)m>L|$o>?15`L>MabbdP5#KL`I;+5opbwH?-meI3fsU{s(;st;a zpi-{gwpD;!0<3uoJS0FW%>kiujRz|_J>6HdG^GRpSOg?*J%vEdCuUYDUab!RKyU!u z26neh0dfp1Z$JU)Gw}#m>=_W(LJPqPvv3%Ior2;3P;Z{+PXW_ec7P|vb3mkV2*@3k zP;RJCP^}!efWvb@XbkAmEGOM*FaeL0CksK&EDiz5$WIBqiab2<=IeU>U>LFNBgb4q5^VN3jS<8G!NGFu?L}abCWrD1hgHWnR-$wWU|7 z#9MqxrGj__grScV42{JB6(~*7d0UeOjqoVOg%WIO^&!`uXTPJO35R>CP6uJRmEyVOS)iOLF4nuELR zX@3Mfcj+0gd>jCk>Lq^;Sm@C%nh#Br8#!1ZAXf1~z#@-uaIp{$0Ofh$0Cx^p>`@sg zTXR4S{|1i$e+LB7S!X)Mcc25hg+B*m4)Jhi9AeF|1Kz^F!6U%m0dI+>bYQ(E&X?DQkF%EDe!3H3}%>lWX9k7&e0A0Ep5(tPo!>%v!9Dp6b zN9)>b@48qARN}`0j!G~HfqVP&hA>RSfW3Yu*Po}B ziB$%?5{u0SU-y)1Pd`lvH1N-A>#>^Qt6<~j24|{DPn{Rj0Wd+J^$MKPJMEncLowM` zg$_%>w^W03H-3%X?ji`xyj6-NxOzxk#=U%~KGHdi08gNn0N#q}eBD)&`dyno*@N;K z9p6L%P6&imnnPsz%po!>^lf^}24I)sRdDnP5}%Zst5?8G2)@XF2NFyE0Fe6tgr~GG zb=M})FHTSB?QRW%$rGR|HLursTt-BnUJhe0?R%0)90lL1C!E?}Cw|Az}! zuREZ)0t=ox!P^|9#S3)>vb$&O`(x`|cy#bOc)GR#AhY0EV#H5AOa8Q-5^48Y2H>p+ zPm^PQ^X#$Yx2=03lvybtR1#sG1PFndRQ|Pl<^9j==e`JUrciK`uann((p$0e~w5q0upMUs|_$E#(0~ zuTBLXBkpwhITf)2{1QR@1rwNxYuk!j<+}&Gir;D=wwab(Jf@bq?g4otu{PIy>)Tu5 z?J^)OkZbmVA?R|-wPoDYmV#VQlq`^?t!%c9b}g1X003%93Jj$<*lze(;JBqWJaP)qrQ%lNhf<0#yZd)M68bbDLCmFVdC}V#1 z8Vx7x(M-e#8AsD*E|q==junbBnc_QfcnMTIoc)z!tc9^_#UtIEs4a#UsJTi z4aCdtLoL{yT>T9nMQ7JQ34|TUJt=N>8?(!vDO9BU67vg?P5=wQQv+gv4+vQr)~E>J zC~;%=9_KU&A9mKlTmk%2pmqM$wQDBsg^v?#CnU zu-)8aN`_|01G~l-v<73zvY;1$|27aBum&g(bSGTERpf`{A3K~V?2Y)^428&gU{)oa zEg_j?$N#=#^BvI=+U7Q}h}Nd*q}}k&i>YA#a6^9C+--?dC4|q@U>_ev%53{s`0q`S zt^3a2-TcT9L;(PQVf8dlub7B&su^GLk(lXO;^TlBYjH6%D-KAo8%tb9R>tRH5-cJT z;wvk0n03I$0qZMqlK8kdiNwEyO_<2+cvao~s;kHR&O&;s>UCAudry9Q?WlWXxR490 zGLA++T-C=X%FNw%m!&j;C=uDO0@d^N*7(_;ihX@`XmZZKK>z5SCu#{lThfV@#K+2#+?B&H|Q<7s5F7dSX7d1r13RY?qv-U9MQ!R6E4)I_f3 zBLH6%%n)gv(O3gaW+;_m-^7R=04_b`H-Gv~$ZtNdai!9~mcuB`I|zKTO1Y3TF+#~3 z!051Az{#Q(`*%S98ry|n)f9LQtQb-vPG_ zEq(djgUVv^0;IZV`!3|O{1bpm5SR~O!-;@x13*_tRRN+u6s9%<6FR;>CeP0N1mE-1 zUC|biwSKK)I{rl4EaLW-PpTh`nW5a9ERjY-lKo)LZ1wJR*U*gQW-zHlx4)q`12KI6 z{c%V=7cbF3e)Fne)=c6vq~SwR<{+5XAg-dx>+*P=|A>`M?L(d;C$SeIFqi1kX)X+; z*22G?0uGKOa?*)>Zgb)P0#n`Q7Ojl%L1glj3V*X3OjV1s^oj!RBL}gL&N>uKZwND817778odLCt*=tt+yA(v#d>(*NY|1D(37)Is2N z$zP{}i~c5on0x@L&NYiJIVGgOrYY`733V;WYYgY0Yz_pgn|^O-oPtCR!g~Oq^ZX3C z@>=Ko43M0H>MTf2-UBTcWc2-;lvdx#-ZWAkey9QXdVYKc2FwZ8ufRE3D7X0$(&;^$ zcAN%%gOWE@lD;O@oPX}{nWDGx-K&X-i6#5q{p5N1;=S4t@Mo~OA2Z@B5{tmF5!HST z>`lzXW{AKYVlTpA3(>gAsGUT)7>6FBP}(3Trxk+OI~fYYVIYwc#EF#0%;-~grt~z3 zZxU>>M#^^R*MDT;XRg__Rl1Xc*bPRx&m+IMc5<5KGTc+Z@M^qR(%yy}n*z8O*xkav zyaki!B$zkAE0H6Q;{3A24KAaR68D=Yct=CVU%>e&@o#G(67PA+xar}yAzww| zBzmO{Com!vjxCYTy+stR7(_P@N{61xIaFp`Yr!u`T8VxL(bXJIvEU7;oDC^nhPe0* z7fglJ+&6Or!f^Q`Q^j!bI7ktB2yD1l5-k%L1@CUUGFT*Vhc=+FC}Y}3g_PKT8vGi) zRkkW)zD$ADk?jWZm=ss?tG9tV<@9 zvXo3&W#9P!avXh&cl>L)s$@0**4m1#{-@^$*T65Z4s7P;keBEOyE(kSz!EFY|Iy8X zijA*-b8$edhgfj8A&Vt_5Em@Jz(5?P|8FA_LzcAr?MM8t8Noe`)9_D8WWyZ(_^f`G zK#;ff>_ZqTVF*OU{=H8-&NhjGONQ@4oDIDQIQp?%{C{Wklma|{yhr~}rDVh3zAt|i zI>cz9tUvhMV;cFV=Zt9m1eNtipyM3#B&tYNcEoPer+nG(m8gmTq1I6|zt#2I-gujV z&yRIX(4!nVQ~ct2-kv>syvbh$!((?laLIRdb#--hb^T}$4haAN000F2f9(we00000 d00000FczS=g-my!^e+Ga002ovPDHLkV1k1kaKZop literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/images/Image-not-found.png b/RobotNet.WebApp/wwwroot/images/Image-not-found.png new file mode 100644 index 0000000000000000000000000000000000000000..9804d3c71c4a64a63ff26aaa3ca48322440ac66f GIT binary patch literal 809 zcmeAS@N?(olHy`uVBq!ia0vp^4?&oN8Ax*GDtQ1Y%K)Dc*Uw+R?c8^2+wK!*FW$U* z>C|^61ISqbD!#J9PH`!{@v9pT2SX;kWNUK7IN2?)|5e=dK+(e(~b9JNr!z zaxgG3t@d&R-~RFI$!jkj5}D6+yS#f*eeI^LQ4xPveORPqDEnQ*wz_t6 zN|dh5r}HMyE1$;9Uv%?o(y6_%Vb!&}KW^OpC|zzz`u-W;Zi&2ock9pJ!e2?2EB@YH zDAT=b+l*RXW8b?ki-k7CQh zvYWy&Gm{RMFA7utVRN*I`*rueD!$_B5qS%*U+`2Oc!k&v1#n<1M>JbhPk8#e@zhmS0xplUB#$JP``LSNQ zYc!TjaN^Qj^1+`g_x^IpLuJkv%fEgO^jP@w(3P3h+9%%sR{QYQ^3KUa9MA2!|DD*K zeeX#3rujAhH*6Gm@#^J+_Y + + + + diff --git a/RobotNet.WebApp/wwwroot/images/logoLight.svg b/RobotNet.WebApp/wwwroot/images/logoLight.svg new file mode 100644 index 0000000..cacd11d --- /dev/null +++ b/RobotNet.WebApp/wwwroot/images/logoLight.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + diff --git a/RobotNet.WebApp/wwwroot/index.html b/RobotNet.WebApp/wwwroot/index.html new file mode 100644 index 0000000..f05f1b7 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/index.html @@ -0,0 +1,58 @@ + + + + + + + RobotNet.WebApp + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RobotNet.WebApp/wwwroot/js/app.js b/RobotNet.WebApp/wwwroot/js/app.js new file mode 100644 index 0000000..1e819d5 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/app.js @@ -0,0 +1,42 @@ +function downloadFile(fileName, content, contentType) { + const blob = new Blob([content], { type: contentType }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +} + +function setFullscreen(isFull) { + const sidebar = document.querySelector('.sidebar'); + const mainContent = document.querySelector('main'); + + if (sidebar && mainContent) { + if (!isFull) { + sidebar.classList.remove('hidden'); + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + sidebar.classList.add('hidden'); + if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(); + } else if (document.documentElement.msRequestFullscreen) { + document.documentElement.msRequestFullscreen(); + } + } + } +} diff --git a/RobotNet.WebApp/wwwroot/js/chart.umd.js b/RobotNet.WebApp/wwwroot/js/chart.umd.js new file mode 100644 index 0000000..bb2afb6 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/chart.umd.js @@ -0,0 +1,14 @@ +/*! + * Chart.js v4.4.6 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener("resize",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.6";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,"complete",wn),xt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=Z(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Ko(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Ko(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=qo(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); +//# sourceMappingURL=chart.umd.js.map diff --git a/RobotNet.WebApp/wwwroot/js/chart.umd.js.map b/RobotNet.WebApp/wwwroot/js/chart.umd.js.map new file mode 100644 index 0000000..2599d43 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/chart.umd.js.map @@ -0,0 +1 @@ +{"version":3,"file":"chart.umd.js","sources":["../src/helpers/helpers.core.ts","../src/helpers/helpers.math.ts","../src/helpers/helpers.collection.ts","../src/helpers/helpers.extras.ts","../src/core/core.animator.js","../node_modules/.pnpm/@kurkle+color@0.3.2/node_modules/@kurkle/color/dist/color.esm.js","../src/helpers/helpers.color.ts","../src/core/core.animations.defaults.js","../src/helpers/helpers.intl.ts","../src/core/core.ticks.js","../src/core/core.defaults.js","../src/core/core.layouts.defaults.js","../src/core/core.scale.defaults.js","../src/helpers/helpers.dom.ts","../src/helpers/helpers.canvas.ts","../src/helpers/helpers.config.ts","../src/helpers/helpers.curve.ts","../src/helpers/helpers.easing.ts","../src/helpers/helpers.interpolation.ts","../src/helpers/helpers.options.ts","../src/helpers/helpers.rtl.ts","../src/helpers/helpers.segment.js","../src/core/core.interaction.js","../src/core/core.layouts.js","../src/platform/platform.base.js","../src/platform/platform.basic.js","../src/platform/platform.dom.js","../src/platform/index.js","../src/core/core.animation.js","../src/core/core.animations.js","../src/core/core.datasetController.js","../src/core/core.element.ts","../src/core/core.scale.autoskip.js","../src/core/core.scale.js","../src/core/core.typedRegistry.js","../src/core/core.registry.js","../src/core/core.plugins.js","../src/core/core.config.js","../src/core/core.controller.js","../src/core/core.adapters.ts","../src/controllers/controller.bar.js","../src/controllers/controller.doughnut.js","../src/controllers/controller.polarArea.js","../src/controllers/controller.bubble.js","../src/controllers/controller.line.js","../src/controllers/controller.pie.js","../src/controllers/controller.radar.js","../src/controllers/controller.scatter.js","../src/elements/element.arc.ts","../src/elements/element.line.js","../src/elements/element.point.ts","../src/elements/element.bar.js","../src/scales/scale.category.js","../src/scales/scale.linearbase.js","../src/scales/scale.linear.js","../src/scales/scale.logarithmic.js","../src/scales/scale.radialLinear.js","../src/scales/scale.time.js","../src/scales/scale.timeseries.js","../src/plugins/plugin.colors.ts","../src/plugins/plugin.decimation.js","../src/plugins/plugin.filler/filler.segment.js","../src/plugins/plugin.filler/filler.helper.js","../src/plugins/plugin.filler/filler.options.js","../src/plugins/plugin.filler/filler.target.stack.js","../src/plugins/plugin.filler/simpleArc.js","../src/plugins/plugin.filler/filler.target.js","../src/plugins/plugin.filler/filler.drawing.js","../src/plugins/plugin.filler/index.js","../src/plugins/plugin.legend.js","../src/plugins/plugin.title.js","../src/plugins/plugin.subtitle.js","../src/plugins/plugin.tooltip.js","../src/index.umd.ts"],"sourcesContent":["/**\n * @namespace Chart.helpers\n */\n\nimport type {AnyObject} from '../types/basic.js';\nimport type {ActiveDataPoint, ChartEvent} from '../types/index.js';\n\n/**\n * An empty function that can be used, for example, for optional callback.\n */\nexport function noop() {\n /* noop */\n}\n\n/**\n * Returns a unique id, sequentially generated from a global variable.\n */\nexport const uid = (() => {\n let id = 0;\n return () => id++;\n})();\n\n/**\n * Returns true if `value` is neither null nor undefined, else returns false.\n * @param value - The value to test.\n * @since 2.7.0\n */\nexport function isNullOrUndef(value: unknown): value is null | undefined {\n return value === null || typeof value === 'undefined';\n}\n\n/**\n * Returns true if `value` is an array (including typed arrays), else returns false.\n * @param value - The value to test.\n * @function\n */\nexport function isArray(value: unknown): value is T[] {\n if (Array.isArray && Array.isArray(value)) {\n return true;\n }\n const type = Object.prototype.toString.call(value);\n if (type.slice(0, 7) === '[object' && type.slice(-6) === 'Array]') {\n return true;\n }\n return false;\n}\n\n/**\n * Returns true if `value` is an object (excluding null), else returns false.\n * @param value - The value to test.\n * @since 2.7.0\n */\nexport function isObject(value: unknown): value is AnyObject {\n return value !== null && Object.prototype.toString.call(value) === '[object Object]';\n}\n\n/**\n * Returns true if `value` is a finite number, else returns false\n * @param value - The value to test.\n */\nfunction isNumberFinite(value: unknown): value is number {\n return (typeof value === 'number' || value instanceof Number) && isFinite(+value);\n}\nexport {\n isNumberFinite as isFinite,\n};\n\n/**\n * Returns `value` if finite, else returns `defaultValue`.\n * @param value - The value to return if defined.\n * @param defaultValue - The value to return if `value` is not finite.\n */\nexport function finiteOrDefault(value: unknown, defaultValue: number) {\n return isNumberFinite(value) ? value : defaultValue;\n}\n\n/**\n * Returns `value` if defined, else returns `defaultValue`.\n * @param value - The value to return if defined.\n * @param defaultValue - The value to return if `value` is undefined.\n */\nexport function valueOrDefault(value: T | undefined, defaultValue: T) {\n return typeof value === 'undefined' ? defaultValue : value;\n}\n\nexport const toPercentage = (value: number | string, dimension: number) =>\n typeof value === 'string' && value.endsWith('%') ?\n parseFloat(value) / 100\n : +value / dimension;\n\nexport const toDimension = (value: number | string, dimension: number) =>\n typeof value === 'string' && value.endsWith('%') ?\n parseFloat(value) / 100 * dimension\n : +value;\n\n/**\n * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the\n * value returned by `fn`. If `fn` is not a function, this method returns undefined.\n * @param fn - The function to call.\n * @param args - The arguments with which `fn` should be called.\n * @param [thisArg] - The value of `this` provided for the call to `fn`.\n */\nexport function callback R, TA, R>(\n fn: T | undefined,\n args: unknown[],\n thisArg?: TA\n): R | undefined {\n if (fn && typeof fn.call === 'function') {\n return fn.apply(thisArg, args);\n }\n}\n\n/**\n * Note(SB) for performance sake, this method should only be used when loopable type\n * is unknown or in none intensive code (not called often and small loopable). Else\n * it's preferable to use a regular for() loop and save extra function calls.\n * @param loopable - The object or array to be iterated.\n * @param fn - The function to call for each item.\n * @param [thisArg] - The value of `this` provided for the call to `fn`.\n * @param [reverse] - If true, iterates backward on the loopable.\n */\nexport function each(\n loopable: Record,\n fn: (this: TA, v: T, i: string) => void,\n thisArg?: TA,\n reverse?: boolean\n): void;\nexport function each(\n loopable: T[],\n fn: (this: TA, v: T, i: number) => void,\n thisArg?: TA,\n reverse?: boolean\n): void;\nexport function each(\n loopable: T[] | Record,\n fn: (this: TA, v: T, i: any) => void,\n thisArg?: TA,\n reverse?: boolean\n) {\n let i: number, len: number, keys: string[];\n if (isArray(loopable)) {\n len = loopable.length;\n if (reverse) {\n for (i = len - 1; i >= 0; i--) {\n fn.call(thisArg, loopable[i], i);\n }\n } else {\n for (i = 0; i < len; i++) {\n fn.call(thisArg, loopable[i], i);\n }\n }\n } else if (isObject(loopable)) {\n keys = Object.keys(loopable);\n len = keys.length;\n for (i = 0; i < len; i++) {\n fn.call(thisArg, loopable[keys[i]], keys[i]);\n }\n }\n}\n\n/**\n * Returns true if the `a0` and `a1` arrays have the same content, else returns false.\n * @param a0 - The array to compare\n * @param a1 - The array to compare\n * @private\n */\nexport function _elementsEqual(a0: ActiveDataPoint[], a1: ActiveDataPoint[]) {\n let i: number, ilen: number, v0: ActiveDataPoint, v1: ActiveDataPoint;\n\n if (!a0 || !a1 || a0.length !== a1.length) {\n return false;\n }\n\n for (i = 0, ilen = a0.length; i < ilen; ++i) {\n v0 = a0[i];\n v1 = a1[i];\n\n if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Returns a deep copy of `source` without keeping references on objects and arrays.\n * @param source - The value to clone.\n */\nexport function clone(source: T): T {\n if (isArray(source)) {\n return source.map(clone) as unknown as T;\n }\n\n if (isObject(source)) {\n const target = Object.create(null);\n const keys = Object.keys(source);\n const klen = keys.length;\n let k = 0;\n\n for (; k < klen; ++k) {\n target[keys[k]] = clone(source[keys[k]]);\n }\n\n return target;\n }\n\n return source;\n}\n\nfunction isValidKey(key: string) {\n return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1;\n}\n\n/**\n * The default merger when Chart.helpers.merge is called without merger option.\n * Note(SB): also used by mergeConfig and mergeScaleConfig as fallback.\n * @private\n */\nexport function _merger(key: string, target: AnyObject, source: AnyObject, options: AnyObject) {\n if (!isValidKey(key)) {\n return;\n }\n\n const tval = target[key];\n const sval = source[key];\n\n if (isObject(tval) && isObject(sval)) {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n merge(tval, sval, options);\n } else {\n target[key] = clone(sval);\n }\n}\n\nexport interface MergeOptions {\n merger?: (key: string, target: AnyObject, source: AnyObject, options?: AnyObject) => void;\n}\n\n/**\n * Recursively deep copies `source` properties into `target` with the given `options`.\n * IMPORTANT: `target` is not cloned and will be updated with `source` properties.\n * @param target - The target object in which all sources are merged into.\n * @param source - Object(s) to merge into `target`.\n * @param [options] - Merging options:\n * @param [options.merger] - The merge method (key, target, source, options)\n * @returns The `target` object.\n */\nexport function merge(target: T, source: [], options?: MergeOptions): T;\nexport function merge(target: T, source: S1, options?: MergeOptions): T & S1;\nexport function merge(target: T, source: [S1], options?: MergeOptions): T & S1;\nexport function merge(target: T, source: [S1, S2], options?: MergeOptions): T & S1 & S2;\nexport function merge(target: T, source: [S1, S2, S3], options?: MergeOptions): T & S1 & S2 & S3;\nexport function merge(\n target: T,\n source: [S1, S2, S3, S4],\n options?: MergeOptions\n): T & S1 & S2 & S3 & S4;\nexport function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject;\nexport function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject {\n const sources = isArray(source) ? source : [source];\n const ilen = sources.length;\n\n if (!isObject(target)) {\n return target as AnyObject;\n }\n\n options = options || {};\n const merger = options.merger || _merger;\n let current: AnyObject;\n\n for (let i = 0; i < ilen; ++i) {\n current = sources[i];\n if (!isObject(current)) {\n continue;\n }\n\n const keys = Object.keys(current);\n for (let k = 0, klen = keys.length; k < klen; ++k) {\n merger(keys[k], target, current, options as AnyObject);\n }\n }\n\n return target;\n}\n\n/**\n * Recursively deep copies `source` properties into `target` *only* if not defined in target.\n * IMPORTANT: `target` is not cloned and will be updated with `source` properties.\n * @param target - The target object in which all sources are merged into.\n * @param source - Object(s) to merge into `target`.\n * @returns The `target` object.\n */\nexport function mergeIf(target: T, source: []): T;\nexport function mergeIf(target: T, source: S1): T & S1;\nexport function mergeIf(target: T, source: [S1]): T & S1;\nexport function mergeIf(target: T, source: [S1, S2]): T & S1 & S2;\nexport function mergeIf(target: T, source: [S1, S2, S3]): T & S1 & S2 & S3;\nexport function mergeIf(target: T, source: [S1, S2, S3, S4]): T & S1 & S2 & S3 & S4;\nexport function mergeIf(target: T, source: AnyObject[]): AnyObject;\nexport function mergeIf(target: T, source: AnyObject[]): AnyObject {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n return merge(target, source, {merger: _mergerIf});\n}\n\n/**\n * Merges source[key] in target[key] only if target[key] is undefined.\n * @private\n */\nexport function _mergerIf(key: string, target: AnyObject, source: AnyObject) {\n if (!isValidKey(key)) {\n return;\n }\n\n const tval = target[key];\n const sval = source[key];\n\n if (isObject(tval) && isObject(sval)) {\n mergeIf(tval, sval);\n } else if (!Object.prototype.hasOwnProperty.call(target, key)) {\n target[key] = clone(sval);\n }\n}\n\n/**\n * @private\n */\nexport function _deprecated(scope: string, value: unknown, previous: string, current: string) {\n if (value !== undefined) {\n console.warn(scope + ': \"' + previous +\n '\" is deprecated. Please use \"' + current + '\" instead');\n }\n}\n\n// resolveObjectKey resolver cache\nconst keyResolvers = {\n // Chart.helpers.core resolveObjectKey should resolve empty key to root object\n '': v => v,\n // default resolvers\n x: o => o.x,\n y: o => o.y\n};\n\n/**\n * @private\n */\nexport function _splitKey(key: string) {\n const parts = key.split('.');\n const keys: string[] = [];\n let tmp = '';\n for (const part of parts) {\n tmp += part;\n if (tmp.endsWith('\\\\')) {\n tmp = tmp.slice(0, -1) + '.';\n } else {\n keys.push(tmp);\n tmp = '';\n }\n }\n return keys;\n}\n\nfunction _getKeyResolver(key: string) {\n const keys = _splitKey(key);\n return obj => {\n for (const k of keys) {\n if (k === '') {\n // For backward compatibility:\n // Chart.helpers.core resolveObjectKey should break at empty key\n break;\n }\n obj = obj && obj[k];\n }\n return obj;\n };\n}\n\nexport function resolveObjectKey(obj: AnyObject, key: string): any {\n const resolver = keyResolvers[key] || (keyResolvers[key] = _getKeyResolver(key));\n return resolver(obj);\n}\n\n/**\n * @private\n */\nexport function _capitalize(str: string) {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n\nexport const defined = (value: unknown) => typeof value !== 'undefined';\n\nexport const isFunction = (value: unknown): value is (...args: any[]) => any => typeof value === 'function';\n\n// Adapted from https://stackoverflow.com/questions/31128855/comparing-ecma6-sets-for-equality#31129384\nexport const setsEqual = (a: Set, b: Set) => {\n if (a.size !== b.size) {\n return false;\n }\n\n for (const item of a) {\n if (!b.has(item)) {\n return false;\n }\n }\n\n return true;\n};\n\n/**\n * @param e - The event\n * @private\n */\nexport function _isClickEvent(e: ChartEvent) {\n return e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu';\n}\n","import type {Point} from '../types/geometric.js';\nimport {isFinite as isFiniteNumber} from './helpers.core.js';\n\n/**\n * @alias Chart.helpers.math\n * @namespace\n */\n\nexport const PI = Math.PI;\nexport const TAU = 2 * PI;\nexport const PITAU = TAU + PI;\nexport const INFINITY = Number.POSITIVE_INFINITY;\nexport const RAD_PER_DEG = PI / 180;\nexport const HALF_PI = PI / 2;\nexport const QUARTER_PI = PI / 4;\nexport const TWO_THIRDS_PI = PI * 2 / 3;\n\nexport const log10 = Math.log10;\nexport const sign = Math.sign;\n\nexport function almostEquals(x: number, y: number, epsilon: number) {\n return Math.abs(x - y) < epsilon;\n}\n\n/**\n * Implementation of the nice number algorithm used in determining where axis labels will go\n */\nexport function niceNum(range: number) {\n const roundedRange = Math.round(range);\n range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range;\n const niceRange = Math.pow(10, Math.floor(log10(range)));\n const fraction = range / niceRange;\n const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;\n return niceFraction * niceRange;\n}\n\n/**\n * Returns an array of factors sorted from 1 to sqrt(value)\n * @private\n */\nexport function _factorize(value: number) {\n const result: number[] = [];\n const sqrt = Math.sqrt(value);\n let i: number;\n\n for (i = 1; i < sqrt; i++) {\n if (value % i === 0) {\n result.push(i);\n result.push(value / i);\n }\n }\n if (sqrt === (sqrt | 0)) { // if value is a square number\n result.push(sqrt);\n }\n\n result.sort((a, b) => a - b).pop();\n return result;\n}\n\nexport function isNumber(n: unknown): n is number {\n return !isNaN(parseFloat(n as string)) && isFinite(n as number);\n}\n\nexport function almostWhole(x: number, epsilon: number) {\n const rounded = Math.round(x);\n return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x);\n}\n\n/**\n * @private\n */\nexport function _setMinAndMaxByKey(\n array: Record[],\n target: { min: number, max: number },\n property: string\n) {\n let i: number, ilen: number, value: number;\n\n for (i = 0, ilen = array.length; i < ilen; i++) {\n value = array[i][property];\n if (!isNaN(value)) {\n target.min = Math.min(target.min, value);\n target.max = Math.max(target.max, value);\n }\n }\n}\n\nexport function toRadians(degrees: number) {\n return degrees * (PI / 180);\n}\n\nexport function toDegrees(radians: number) {\n return radians * (180 / PI);\n}\n\n/**\n * Returns the number of decimal places\n * i.e. the number of digits after the decimal point, of the value of this Number.\n * @param x - A number.\n * @returns The number of decimal places.\n * @private\n */\nexport function _decimalPlaces(x: number) {\n if (!isFiniteNumber(x)) {\n return;\n }\n let e = 1;\n let p = 0;\n while (Math.round(x * e) / e !== x) {\n e *= 10;\n p++;\n }\n return p;\n}\n\n// Gets the angle from vertical upright to the point about a centre.\nexport function getAngleFromPoint(\n centrePoint: Point,\n anglePoint: Point\n) {\n const distanceFromXCenter = anglePoint.x - centrePoint.x;\n const distanceFromYCenter = anglePoint.y - centrePoint.y;\n const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);\n\n let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter);\n\n if (angle < (-0.5 * PI)) {\n angle += TAU; // make sure the returned angle is in the range of (-PI/2, 3PI/2]\n }\n\n return {\n angle,\n distance: radialDistanceFromCenter\n };\n}\n\nexport function distanceBetweenPoints(pt1: Point, pt2: Point) {\n return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));\n}\n\n/**\n * Shortest distance between angles, in either direction.\n * @private\n */\nexport function _angleDiff(a: number, b: number) {\n return (a - b + PITAU) % TAU - PI;\n}\n\n/**\n * Normalize angle to be between 0 and 2*PI\n * @private\n */\nexport function _normalizeAngle(a: number) {\n return (a % TAU + TAU) % TAU;\n}\n\n/**\n * @private\n */\nexport function _angleBetween(angle: number, start: number, end: number, sameAngleIsFullCircle?: boolean) {\n const a = _normalizeAngle(angle);\n const s = _normalizeAngle(start);\n const e = _normalizeAngle(end);\n const angleToStart = _normalizeAngle(s - a);\n const angleToEnd = _normalizeAngle(e - a);\n const startToAngle = _normalizeAngle(a - s);\n const endToAngle = _normalizeAngle(a - e);\n return a === s || a === e || (sameAngleIsFullCircle && s === e)\n || (angleToStart > angleToEnd && startToAngle < endToAngle);\n}\n\n/**\n * Limit `value` between `min` and `max`\n * @param value\n * @param min\n * @param max\n * @private\n */\nexport function _limitValue(value: number, min: number, max: number) {\n return Math.max(min, Math.min(max, value));\n}\n\n/**\n * @param {number} value\n * @private\n */\nexport function _int16Range(value: number) {\n return _limitValue(value, -32768, 32767);\n}\n\n/**\n * @param value\n * @param start\n * @param end\n * @param [epsilon]\n * @private\n */\nexport function _isBetween(value: number, start: number, end: number, epsilon = 1e-6) {\n return value >= Math.min(start, end) - epsilon && value <= Math.max(start, end) + epsilon;\n}\n","import {_capitalize} from './helpers.core.js';\n\n/**\n * Binary search\n * @param table - the table search. must be sorted!\n * @param value - value to find\n * @param cmp\n * @private\n */\nexport function _lookup(\n table: number[],\n value: number,\n cmp?: (value: number) => boolean\n): {lo: number, hi: number};\nexport function _lookup(\n table: T[],\n value: number,\n cmp: (value: number) => boolean\n): {lo: number, hi: number};\nexport function _lookup(\n table: unknown[],\n value: number,\n cmp?: (value: number) => boolean\n) {\n cmp = cmp || ((index) => table[index] < value);\n let hi = table.length - 1;\n let lo = 0;\n let mid: number;\n\n while (hi - lo > 1) {\n mid = (lo + hi) >> 1;\n if (cmp(mid)) {\n lo = mid;\n } else {\n hi = mid;\n }\n }\n\n return {lo, hi};\n}\n\n/**\n * Binary search\n * @param table - the table search. must be sorted!\n * @param key - property name for the value in each entry\n * @param value - value to find\n * @param last - lookup last index\n * @private\n */\nexport const _lookupByKey = (\n table: Record[],\n key: string,\n value: number,\n last?: boolean\n) =>\n _lookup(table, value, last\n ? index => {\n const ti = table[index][key];\n return ti < value || ti === value && table[index + 1][key] === value;\n }\n : index => table[index][key] < value);\n\n/**\n * Reverse binary search\n * @param table - the table search. must be sorted!\n * @param key - property name for the value in each entry\n * @param value - value to find\n * @private\n */\nexport const _rlookupByKey = (\n table: Record[],\n key: string,\n value: number\n) =>\n _lookup(table, value, index => table[index][key] >= value);\n\n/**\n * Return subset of `values` between `min` and `max` inclusive.\n * Values are assumed to be in sorted order.\n * @param values - sorted array of values\n * @param min - min value\n * @param max - max value\n */\nexport function _filterBetween(values: number[], min: number, max: number) {\n let start = 0;\n let end = values.length;\n\n while (start < end && values[start] < min) {\n start++;\n }\n while (end > start && values[end - 1] > max) {\n end--;\n }\n\n return start > 0 || end < values.length\n ? values.slice(start, end)\n : values;\n}\n\nconst arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift'] as const;\n\nexport interface ArrayListener {\n _onDataPush?(...item: T[]): void;\n _onDataPop?(): void;\n _onDataShift?(): void;\n _onDataSplice?(index: number, deleteCount: number, ...items: T[]): void;\n _onDataUnshift?(...item: T[]): void;\n}\n\n/**\n * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice',\n * 'unshift') and notify the listener AFTER the array has been altered. Listeners are\n * called on the '_onData*' callbacks (e.g. _onDataPush, etc.) with same arguments.\n */\nexport function listenArrayEvents(array: T[], listener: ArrayListener): void;\nexport function listenArrayEvents(array, listener) {\n if (array._chartjs) {\n array._chartjs.listeners.push(listener);\n return;\n }\n\n Object.defineProperty(array, '_chartjs', {\n configurable: true,\n enumerable: false,\n value: {\n listeners: [listener]\n }\n });\n\n arrayEvents.forEach((key) => {\n const method = '_onData' + _capitalize(key);\n const base = array[key];\n\n Object.defineProperty(array, key, {\n configurable: true,\n enumerable: false,\n value(...args) {\n const res = base.apply(this, args);\n\n array._chartjs.listeners.forEach((object) => {\n if (typeof object[method] === 'function') {\n object[method](...args);\n }\n });\n\n return res;\n }\n });\n });\n}\n\n\n/**\n * Removes the given array event listener and cleanup extra attached properties (such as\n * the _chartjs stub and overridden methods) if array doesn't have any more listeners.\n */\nexport function unlistenArrayEvents(array: T[], listener: ArrayListener): void;\nexport function unlistenArrayEvents(array, listener) {\n const stub = array._chartjs;\n if (!stub) {\n return;\n }\n\n const listeners = stub.listeners;\n const index = listeners.indexOf(listener);\n if (index !== -1) {\n listeners.splice(index, 1);\n }\n\n if (listeners.length > 0) {\n return;\n }\n\n arrayEvents.forEach((key) => {\n delete array[key];\n });\n\n delete array._chartjs;\n}\n\n/**\n * @param items\n */\nexport function _arrayUnique(items: T[]) {\n const set = new Set(items);\n\n if (set.size === items.length) {\n return items;\n }\n\n return Array.from(set);\n}\n","import type {ChartMeta, PointElement} from '../types/index.js';\n\nimport {_limitValue} from './helpers.math.js';\nimport {_lookupByKey} from './helpers.collection.js';\n\nexport function fontString(pixelSize: number, fontStyle: string, fontFamily: string) {\n return fontStyle + ' ' + pixelSize + 'px ' + fontFamily;\n}\n\n/**\n* Request animation polyfill\n*/\nexport const requestAnimFrame = (function() {\n if (typeof window === 'undefined') {\n return function(callback) {\n return callback();\n };\n }\n return window.requestAnimationFrame;\n}());\n\n/**\n * Throttles calling `fn` once per animation frame\n * Latest arguments are used on the actual call\n */\nexport function throttled>(\n fn: (...args: TArgs) => void,\n thisArg: any,\n) {\n let argsToUse = [] as TArgs;\n let ticking = false;\n\n return function(...args: TArgs) {\n // Save the args for use later\n argsToUse = args;\n if (!ticking) {\n ticking = true;\n requestAnimFrame.call(window, () => {\n ticking = false;\n fn.apply(thisArg, argsToUse);\n });\n }\n };\n}\n\n/**\n * Debounces calling `fn` for `delay` ms\n */\nexport function debounce>(fn: (...args: TArgs) => void, delay: number) {\n let timeout;\n return function(...args: TArgs) {\n if (delay) {\n clearTimeout(timeout);\n timeout = setTimeout(fn, delay, args);\n } else {\n fn.apply(this, args);\n }\n return delay;\n };\n}\n\n/**\n * Converts 'start' to 'left', 'end' to 'right' and others to 'center'\n * @private\n */\nexport const _toLeftRightCenter = (align: 'start' | 'end' | 'center') => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center';\n\n/**\n * Returns `start`, `end` or `(start + end) / 2` depending on `align`. Defaults to `center`\n * @private\n */\nexport const _alignStartEnd = (align: 'start' | 'end' | 'center', start: number, end: number) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2;\n\n/**\n * Returns `left`, `right` or `(left + right) / 2` depending on `align`. Defaults to `left`\n * @private\n */\nexport const _textX = (align: 'left' | 'right' | 'center', left: number, right: number, rtl: boolean) => {\n const check = rtl ? 'left' : 'right';\n return align === check ? right : align === 'center' ? (left + right) / 2 : left;\n};\n\n/**\n * Return start and count of visible points.\n * @private\n */\nexport function _getStartAndCountOfVisiblePoints(meta: ChartMeta<'line' | 'scatter'>, points: PointElement[], animationsDisabled: boolean) {\n const pointCount = points.length;\n\n let start = 0;\n let count = pointCount;\n\n if (meta._sorted) {\n const {iScale, _parsed} = meta;\n const axis = iScale.axis;\n const {min, max, minDefined, maxDefined} = iScale.getUserBounds();\n\n if (minDefined) {\n start = _limitValue(Math.min(\n // @ts-expect-error Need to type _parsed\n _lookupByKey(_parsed, axis, min).lo,\n // @ts-expect-error Need to fix types on _lookupByKey\n animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo),\n 0, pointCount - 1);\n }\n if (maxDefined) {\n count = _limitValue(Math.max(\n // @ts-expect-error Need to type _parsed\n _lookupByKey(_parsed, iScale.axis, max, true).hi + 1,\n // @ts-expect-error Need to fix types on _lookupByKey\n animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1),\n start, pointCount) - start;\n } else {\n count = pointCount - start;\n }\n }\n\n return {start, count};\n}\n\n/**\n * Checks if the scale ranges have changed.\n * @param {object} meta - dataset meta.\n * @returns {boolean}\n * @private\n */\nexport function _scaleRangesChanged(meta) {\n const {xScale, yScale, _scaleRanges} = meta;\n const newRanges = {\n xmin: xScale.min,\n xmax: xScale.max,\n ymin: yScale.min,\n ymax: yScale.max\n };\n if (!_scaleRanges) {\n meta._scaleRanges = newRanges;\n return true;\n }\n const changed = _scaleRanges.xmin !== xScale.min\n\t\t|| _scaleRanges.xmax !== xScale.max\n\t\t|| _scaleRanges.ymin !== yScale.min\n\t\t|| _scaleRanges.ymax !== yScale.max;\n\n Object.assign(_scaleRanges, newRanges);\n return changed;\n}\n","import {requestAnimFrame} from '../helpers/helpers.extras.js';\n\n/**\n * @typedef { import('./core.animation.js').default } Animation\n * @typedef { import('./core.controller.js').default } Chart\n */\n\n/**\n * Please use the module's default export which provides a singleton instance\n * Note: class is export for typedoc\n */\nexport class Animator {\n constructor() {\n this._request = null;\n this._charts = new Map();\n this._running = false;\n this._lastDate = undefined;\n }\n\n /**\n\t * @private\n\t */\n _notify(chart, anims, date, type) {\n const callbacks = anims.listeners[type];\n const numSteps = anims.duration;\n\n callbacks.forEach(fn => fn({\n chart,\n initial: anims.initial,\n numSteps,\n currentStep: Math.min(date - anims.start, numSteps)\n }));\n }\n\n /**\n\t * @private\n\t */\n _refresh() {\n if (this._request) {\n return;\n }\n this._running = true;\n\n this._request = requestAnimFrame.call(window, () => {\n this._update();\n this._request = null;\n\n if (this._running) {\n this._refresh();\n }\n });\n }\n\n /**\n\t * @private\n\t */\n _update(date = Date.now()) {\n let remaining = 0;\n\n this._charts.forEach((anims, chart) => {\n if (!anims.running || !anims.items.length) {\n return;\n }\n const items = anims.items;\n let i = items.length - 1;\n let draw = false;\n let item;\n\n for (; i >= 0; --i) {\n item = items[i];\n\n if (item._active) {\n if (item._total > anims.duration) {\n // if the animation has been updated and its duration prolonged,\n // update to total duration of current animations run (for progress event)\n anims.duration = item._total;\n }\n item.tick(date);\n draw = true;\n } else {\n // Remove the item by replacing it with last item and removing the last\n // A lot faster than splice.\n items[i] = items[items.length - 1];\n items.pop();\n }\n }\n\n if (draw) {\n chart.draw();\n this._notify(chart, anims, date, 'progress');\n }\n\n if (!items.length) {\n anims.running = false;\n this._notify(chart, anims, date, 'complete');\n anims.initial = false;\n }\n\n remaining += items.length;\n });\n\n this._lastDate = date;\n\n if (remaining === 0) {\n this._running = false;\n }\n }\n\n /**\n\t * @private\n\t */\n _getAnims(chart) {\n const charts = this._charts;\n let anims = charts.get(chart);\n if (!anims) {\n anims = {\n running: false,\n initial: true,\n items: [],\n listeners: {\n complete: [],\n progress: []\n }\n };\n charts.set(chart, anims);\n }\n return anims;\n }\n\n /**\n\t * @param {Chart} chart\n\t * @param {string} event - event name\n\t * @param {Function} cb - callback\n\t */\n listen(chart, event, cb) {\n this._getAnims(chart).listeners[event].push(cb);\n }\n\n /**\n\t * Add animations\n\t * @param {Chart} chart\n\t * @param {Animation[]} items - animations\n\t */\n add(chart, items) {\n if (!items || !items.length) {\n return;\n }\n this._getAnims(chart).items.push(...items);\n }\n\n /**\n\t * Counts number of active animations for the chart\n\t * @param {Chart} chart\n\t */\n has(chart) {\n return this._getAnims(chart).items.length > 0;\n }\n\n /**\n\t * Start animating (all charts)\n\t * @param {Chart} chart\n\t */\n start(chart) {\n const anims = this._charts.get(chart);\n if (!anims) {\n return;\n }\n anims.running = true;\n anims.start = Date.now();\n anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0);\n this._refresh();\n }\n\n running(chart) {\n if (!this._running) {\n return false;\n }\n const anims = this._charts.get(chart);\n if (!anims || !anims.running || !anims.items.length) {\n return false;\n }\n return true;\n }\n\n /**\n\t * Stop all animations for the chart\n\t * @param {Chart} chart\n\t */\n stop(chart) {\n const anims = this._charts.get(chart);\n if (!anims || !anims.items.length) {\n return;\n }\n const items = anims.items;\n let i = items.length - 1;\n\n for (; i >= 0; --i) {\n items[i].cancel();\n }\n anims.items = [];\n this._notify(chart, anims, Date.now(), 'complete');\n }\n\n /**\n\t * Remove chart from Animator\n\t * @param {Chart} chart\n\t */\n remove(chart) {\n return this._charts.delete(chart);\n }\n}\n\n// singleton instance\nexport default /* #__PURE__ */ new Animator();\n","/*!\n * @kurkle/color v0.3.2\n * https://github.com/kurkle/color#readme\n * (c) 2023 Jukka Kurkela\n * Released under the MIT License\n */\nfunction round(v) {\n return v + 0.5 | 0;\n}\nconst lim = (v, l, h) => Math.max(Math.min(v, h), l);\nfunction p2b(v) {\n return lim(round(v * 2.55), 0, 255);\n}\nfunction b2p(v) {\n return lim(round(v / 2.55), 0, 100);\n}\nfunction n2b(v) {\n return lim(round(v * 255), 0, 255);\n}\nfunction b2n(v) {\n return lim(round(v / 2.55) / 100, 0, 1);\n}\nfunction n2p(v) {\n return lim(round(v * 100), 0, 100);\n}\n\nconst map$1 = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, A: 10, B: 11, C: 12, D: 13, E: 14, F: 15, a: 10, b: 11, c: 12, d: 13, e: 14, f: 15};\nconst hex = [...'0123456789ABCDEF'];\nconst h1 = b => hex[b & 0xF];\nconst h2 = b => hex[(b & 0xF0) >> 4] + hex[b & 0xF];\nconst eq = b => ((b & 0xF0) >> 4) === (b & 0xF);\nconst isShort = v => eq(v.r) && eq(v.g) && eq(v.b) && eq(v.a);\nfunction hexParse(str) {\n var len = str.length;\n var ret;\n if (str[0] === '#') {\n if (len === 4 || len === 5) {\n ret = {\n r: 255 & map$1[str[1]] * 17,\n g: 255 & map$1[str[2]] * 17,\n b: 255 & map$1[str[3]] * 17,\n a: len === 5 ? map$1[str[4]] * 17 : 255\n };\n } else if (len === 7 || len === 9) {\n ret = {\n r: map$1[str[1]] << 4 | map$1[str[2]],\n g: map$1[str[3]] << 4 | map$1[str[4]],\n b: map$1[str[5]] << 4 | map$1[str[6]],\n a: len === 9 ? (map$1[str[7]] << 4 | map$1[str[8]]) : 255\n };\n }\n }\n return ret;\n}\nconst alpha = (a, f) => a < 255 ? f(a) : '';\nfunction hexString(v) {\n var f = isShort(v) ? h1 : h2;\n return v\n ? '#' + f(v.r) + f(v.g) + f(v.b) + alpha(v.a, f)\n : undefined;\n}\n\nconst HUE_RE = /^(hsla?|hwb|hsv)\\(\\s*([-+.e\\d]+)(?:deg)?[\\s,]+([-+.e\\d]+)%[\\s,]+([-+.e\\d]+)%(?:[\\s,]+([-+.e\\d]+)(%)?)?\\s*\\)$/;\nfunction hsl2rgbn(h, s, l) {\n const a = s * Math.min(l, 1 - l);\n const f = (n, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);\n return [f(0), f(8), f(4)];\n}\nfunction hsv2rgbn(h, s, v) {\n const f = (n, k = (n + h / 60) % 6) => v - v * s * Math.max(Math.min(k, 4 - k, 1), 0);\n return [f(5), f(3), f(1)];\n}\nfunction hwb2rgbn(h, w, b) {\n const rgb = hsl2rgbn(h, 1, 0.5);\n let i;\n if (w + b > 1) {\n i = 1 / (w + b);\n w *= i;\n b *= i;\n }\n for (i = 0; i < 3; i++) {\n rgb[i] *= 1 - w - b;\n rgb[i] += w;\n }\n return rgb;\n}\nfunction hueValue(r, g, b, d, max) {\n if (r === max) {\n return ((g - b) / d) + (g < b ? 6 : 0);\n }\n if (g === max) {\n return (b - r) / d + 2;\n }\n return (r - g) / d + 4;\n}\nfunction rgb2hsl(v) {\n const range = 255;\n const r = v.r / range;\n const g = v.g / range;\n const b = v.b / range;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const l = (max + min) / 2;\n let h, s, d;\n if (max !== min) {\n d = max - min;\n s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n h = hueValue(r, g, b, d, max);\n h = h * 60 + 0.5;\n }\n return [h | 0, s || 0, l];\n}\nfunction calln(f, a, b, c) {\n return (\n Array.isArray(a)\n ? f(a[0], a[1], a[2])\n : f(a, b, c)\n ).map(n2b);\n}\nfunction hsl2rgb(h, s, l) {\n return calln(hsl2rgbn, h, s, l);\n}\nfunction hwb2rgb(h, w, b) {\n return calln(hwb2rgbn, h, w, b);\n}\nfunction hsv2rgb(h, s, v) {\n return calln(hsv2rgbn, h, s, v);\n}\nfunction hue(h) {\n return (h % 360 + 360) % 360;\n}\nfunction hueParse(str) {\n const m = HUE_RE.exec(str);\n let a = 255;\n let v;\n if (!m) {\n return;\n }\n if (m[5] !== v) {\n a = m[6] ? p2b(+m[5]) : n2b(+m[5]);\n }\n const h = hue(+m[2]);\n const p1 = +m[3] / 100;\n const p2 = +m[4] / 100;\n if (m[1] === 'hwb') {\n v = hwb2rgb(h, p1, p2);\n } else if (m[1] === 'hsv') {\n v = hsv2rgb(h, p1, p2);\n } else {\n v = hsl2rgb(h, p1, p2);\n }\n return {\n r: v[0],\n g: v[1],\n b: v[2],\n a: a\n };\n}\nfunction rotate(v, deg) {\n var h = rgb2hsl(v);\n h[0] = hue(h[0] + deg);\n h = hsl2rgb(h);\n v.r = h[0];\n v.g = h[1];\n v.b = h[2];\n}\nfunction hslString(v) {\n if (!v) {\n return;\n }\n const a = rgb2hsl(v);\n const h = a[0];\n const s = n2p(a[1]);\n const l = n2p(a[2]);\n return v.a < 255\n ? `hsla(${h}, ${s}%, ${l}%, ${b2n(v.a)})`\n : `hsl(${h}, ${s}%, ${l}%)`;\n}\n\nconst map = {\n x: 'dark',\n Z: 'light',\n Y: 're',\n X: 'blu',\n W: 'gr',\n V: 'medium',\n U: 'slate',\n A: 'ee',\n T: 'ol',\n S: 'or',\n B: 'ra',\n C: 'lateg',\n D: 'ights',\n R: 'in',\n Q: 'turquois',\n E: 'hi',\n P: 'ro',\n O: 'al',\n N: 'le',\n M: 'de',\n L: 'yello',\n F: 'en',\n K: 'ch',\n G: 'arks',\n H: 'ea',\n I: 'ightg',\n J: 'wh'\n};\nconst names$1 = {\n OiceXe: 'f0f8ff',\n antiquewEte: 'faebd7',\n aqua: 'ffff',\n aquamarRe: '7fffd4',\n azuY: 'f0ffff',\n beige: 'f5f5dc',\n bisque: 'ffe4c4',\n black: '0',\n blanKedOmond: 'ffebcd',\n Xe: 'ff',\n XeviTet: '8a2be2',\n bPwn: 'a52a2a',\n burlywood: 'deb887',\n caMtXe: '5f9ea0',\n KartYuse: '7fff00',\n KocTate: 'd2691e',\n cSO: 'ff7f50',\n cSnflowerXe: '6495ed',\n cSnsilk: 'fff8dc',\n crimson: 'dc143c',\n cyan: 'ffff',\n xXe: '8b',\n xcyan: '8b8b',\n xgTMnPd: 'b8860b',\n xWay: 'a9a9a9',\n xgYF: '6400',\n xgYy: 'a9a9a9',\n xkhaki: 'bdb76b',\n xmagFta: '8b008b',\n xTivegYF: '556b2f',\n xSange: 'ff8c00',\n xScEd: '9932cc',\n xYd: '8b0000',\n xsOmon: 'e9967a',\n xsHgYF: '8fbc8f',\n xUXe: '483d8b',\n xUWay: '2f4f4f',\n xUgYy: '2f4f4f',\n xQe: 'ced1',\n xviTet: '9400d3',\n dAppRk: 'ff1493',\n dApskyXe: 'bfff',\n dimWay: '696969',\n dimgYy: '696969',\n dodgerXe: '1e90ff',\n fiYbrick: 'b22222',\n flSOwEte: 'fffaf0',\n foYstWAn: '228b22',\n fuKsia: 'ff00ff',\n gaRsbSo: 'dcdcdc',\n ghostwEte: 'f8f8ff',\n gTd: 'ffd700',\n gTMnPd: 'daa520',\n Way: '808080',\n gYF: '8000',\n gYFLw: 'adff2f',\n gYy: '808080',\n honeyMw: 'f0fff0',\n hotpRk: 'ff69b4',\n RdianYd: 'cd5c5c',\n Rdigo: '4b0082',\n ivSy: 'fffff0',\n khaki: 'f0e68c',\n lavFMr: 'e6e6fa',\n lavFMrXsh: 'fff0f5',\n lawngYF: '7cfc00',\n NmoncEffon: 'fffacd',\n ZXe: 'add8e6',\n ZcSO: 'f08080',\n Zcyan: 'e0ffff',\n ZgTMnPdLw: 'fafad2',\n ZWay: 'd3d3d3',\n ZgYF: '90ee90',\n ZgYy: 'd3d3d3',\n ZpRk: 'ffb6c1',\n ZsOmon: 'ffa07a',\n ZsHgYF: '20b2aa',\n ZskyXe: '87cefa',\n ZUWay: '778899',\n ZUgYy: '778899',\n ZstAlXe: 'b0c4de',\n ZLw: 'ffffe0',\n lime: 'ff00',\n limegYF: '32cd32',\n lRF: 'faf0e6',\n magFta: 'ff00ff',\n maPon: '800000',\n VaquamarRe: '66cdaa',\n VXe: 'cd',\n VScEd: 'ba55d3',\n VpurpN: '9370db',\n VsHgYF: '3cb371',\n VUXe: '7b68ee',\n VsprRggYF: 'fa9a',\n VQe: '48d1cc',\n VviTetYd: 'c71585',\n midnightXe: '191970',\n mRtcYam: 'f5fffa',\n mistyPse: 'ffe4e1',\n moccasR: 'ffe4b5',\n navajowEte: 'ffdead',\n navy: '80',\n Tdlace: 'fdf5e6',\n Tive: '808000',\n TivedBb: '6b8e23',\n Sange: 'ffa500',\n SangeYd: 'ff4500',\n ScEd: 'da70d6',\n pOegTMnPd: 'eee8aa',\n pOegYF: '98fb98',\n pOeQe: 'afeeee',\n pOeviTetYd: 'db7093',\n papayawEp: 'ffefd5',\n pHKpuff: 'ffdab9',\n peru: 'cd853f',\n pRk: 'ffc0cb',\n plum: 'dda0dd',\n powMrXe: 'b0e0e6',\n purpN: '800080',\n YbeccapurpN: '663399',\n Yd: 'ff0000',\n Psybrown: 'bc8f8f',\n PyOXe: '4169e1',\n saddNbPwn: '8b4513',\n sOmon: 'fa8072',\n sandybPwn: 'f4a460',\n sHgYF: '2e8b57',\n sHshell: 'fff5ee',\n siFna: 'a0522d',\n silver: 'c0c0c0',\n skyXe: '87ceeb',\n UXe: '6a5acd',\n UWay: '708090',\n UgYy: '708090',\n snow: 'fffafa',\n sprRggYF: 'ff7f',\n stAlXe: '4682b4',\n tan: 'd2b48c',\n teO: '8080',\n tEstN: 'd8bfd8',\n tomato: 'ff6347',\n Qe: '40e0d0',\n viTet: 'ee82ee',\n JHt: 'f5deb3',\n wEte: 'ffffff',\n wEtesmoke: 'f5f5f5',\n Lw: 'ffff00',\n LwgYF: '9acd32'\n};\nfunction unpack() {\n const unpacked = {};\n const keys = Object.keys(names$1);\n const tkeys = Object.keys(map);\n let i, j, k, ok, nk;\n for (i = 0; i < keys.length; i++) {\n ok = nk = keys[i];\n for (j = 0; j < tkeys.length; j++) {\n k = tkeys[j];\n nk = nk.replace(k, map[k]);\n }\n k = parseInt(names$1[ok], 16);\n unpacked[nk] = [k >> 16 & 0xFF, k >> 8 & 0xFF, k & 0xFF];\n }\n return unpacked;\n}\n\nlet names;\nfunction nameParse(str) {\n if (!names) {\n names = unpack();\n names.transparent = [0, 0, 0, 0];\n }\n const a = names[str.toLowerCase()];\n return a && {\n r: a[0],\n g: a[1],\n b: a[2],\n a: a.length === 4 ? a[3] : 255\n };\n}\n\nconst RGB_RE = /^rgba?\\(\\s*([-+.\\d]+)(%)?[\\s,]+([-+.e\\d]+)(%)?[\\s,]+([-+.e\\d]+)(%)?(?:[\\s,/]+([-+.e\\d]+)(%)?)?\\s*\\)$/;\nfunction rgbParse(str) {\n const m = RGB_RE.exec(str);\n let a = 255;\n let r, g, b;\n if (!m) {\n return;\n }\n if (m[7] !== r) {\n const v = +m[7];\n a = m[8] ? p2b(v) : lim(v * 255, 0, 255);\n }\n r = +m[1];\n g = +m[3];\n b = +m[5];\n r = 255 & (m[2] ? p2b(r) : lim(r, 0, 255));\n g = 255 & (m[4] ? p2b(g) : lim(g, 0, 255));\n b = 255 & (m[6] ? p2b(b) : lim(b, 0, 255));\n return {\n r: r,\n g: g,\n b: b,\n a: a\n };\n}\nfunction rgbString(v) {\n return v && (\n v.a < 255\n ? `rgba(${v.r}, ${v.g}, ${v.b}, ${b2n(v.a)})`\n : `rgb(${v.r}, ${v.g}, ${v.b})`\n );\n}\n\nconst to = v => v <= 0.0031308 ? v * 12.92 : Math.pow(v, 1.0 / 2.4) * 1.055 - 0.055;\nconst from = v => v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\nfunction interpolate(rgb1, rgb2, t) {\n const r = from(b2n(rgb1.r));\n const g = from(b2n(rgb1.g));\n const b = from(b2n(rgb1.b));\n return {\n r: n2b(to(r + t * (from(b2n(rgb2.r)) - r))),\n g: n2b(to(g + t * (from(b2n(rgb2.g)) - g))),\n b: n2b(to(b + t * (from(b2n(rgb2.b)) - b))),\n a: rgb1.a + t * (rgb2.a - rgb1.a)\n };\n}\n\nfunction modHSL(v, i, ratio) {\n if (v) {\n let tmp = rgb2hsl(v);\n tmp[i] = Math.max(0, Math.min(tmp[i] + tmp[i] * ratio, i === 0 ? 360 : 1));\n tmp = hsl2rgb(tmp);\n v.r = tmp[0];\n v.g = tmp[1];\n v.b = tmp[2];\n }\n}\nfunction clone(v, proto) {\n return v ? Object.assign(proto || {}, v) : v;\n}\nfunction fromObject(input) {\n var v = {r: 0, g: 0, b: 0, a: 255};\n if (Array.isArray(input)) {\n if (input.length >= 3) {\n v = {r: input[0], g: input[1], b: input[2], a: 255};\n if (input.length > 3) {\n v.a = n2b(input[3]);\n }\n }\n } else {\n v = clone(input, {r: 0, g: 0, b: 0, a: 1});\n v.a = n2b(v.a);\n }\n return v;\n}\nfunction functionParse(str) {\n if (str.charAt(0) === 'r') {\n return rgbParse(str);\n }\n return hueParse(str);\n}\nclass Color {\n constructor(input) {\n if (input instanceof Color) {\n return input;\n }\n const type = typeof input;\n let v;\n if (type === 'object') {\n v = fromObject(input);\n } else if (type === 'string') {\n v = hexParse(input) || nameParse(input) || functionParse(input);\n }\n this._rgb = v;\n this._valid = !!v;\n }\n get valid() {\n return this._valid;\n }\n get rgb() {\n var v = clone(this._rgb);\n if (v) {\n v.a = b2n(v.a);\n }\n return v;\n }\n set rgb(obj) {\n this._rgb = fromObject(obj);\n }\n rgbString() {\n return this._valid ? rgbString(this._rgb) : undefined;\n }\n hexString() {\n return this._valid ? hexString(this._rgb) : undefined;\n }\n hslString() {\n return this._valid ? hslString(this._rgb) : undefined;\n }\n mix(color, weight) {\n if (color) {\n const c1 = this.rgb;\n const c2 = color.rgb;\n let w2;\n const p = weight === w2 ? 0.5 : weight;\n const w = 2 * p - 1;\n const a = c1.a - c2.a;\n const w1 = ((w * a === -1 ? w : (w + a) / (1 + w * a)) + 1) / 2.0;\n w2 = 1 - w1;\n c1.r = 0xFF & w1 * c1.r + w2 * c2.r + 0.5;\n c1.g = 0xFF & w1 * c1.g + w2 * c2.g + 0.5;\n c1.b = 0xFF & w1 * c1.b + w2 * c2.b + 0.5;\n c1.a = p * c1.a + (1 - p) * c2.a;\n this.rgb = c1;\n }\n return this;\n }\n interpolate(color, t) {\n if (color) {\n this._rgb = interpolate(this._rgb, color._rgb, t);\n }\n return this;\n }\n clone() {\n return new Color(this.rgb);\n }\n alpha(a) {\n this._rgb.a = n2b(a);\n return this;\n }\n clearer(ratio) {\n const rgb = this._rgb;\n rgb.a *= 1 - ratio;\n return this;\n }\n greyscale() {\n const rgb = this._rgb;\n const val = round(rgb.r * 0.3 + rgb.g * 0.59 + rgb.b * 0.11);\n rgb.r = rgb.g = rgb.b = val;\n return this;\n }\n opaquer(ratio) {\n const rgb = this._rgb;\n rgb.a *= 1 + ratio;\n return this;\n }\n negate() {\n const v = this._rgb;\n v.r = 255 - v.r;\n v.g = 255 - v.g;\n v.b = 255 - v.b;\n return this;\n }\n lighten(ratio) {\n modHSL(this._rgb, 2, ratio);\n return this;\n }\n darken(ratio) {\n modHSL(this._rgb, 2, -ratio);\n return this;\n }\n saturate(ratio) {\n modHSL(this._rgb, 1, ratio);\n return this;\n }\n desaturate(ratio) {\n modHSL(this._rgb, 1, -ratio);\n return this;\n }\n rotate(deg) {\n rotate(this._rgb, deg);\n return this;\n }\n}\n\nfunction index_esm(input) {\n return new Color(input);\n}\n\nexport { Color, b2n, b2p, index_esm as default, hexParse, hexString, hsl2rgb, hslString, hsv2rgb, hueParse, hwb2rgb, lim, n2b, n2p, nameParse, p2b, rgb2hsl, rgbParse, rgbString, rotate, round };\n","import {Color} from '@kurkle/color';\n\nexport function isPatternOrGradient(value: unknown): value is CanvasPattern | CanvasGradient {\n if (value && typeof value === 'object') {\n const type = value.toString();\n return type === '[object CanvasPattern]' || type === '[object CanvasGradient]';\n }\n\n return false;\n}\n\nexport function color(value: CanvasGradient): CanvasGradient;\nexport function color(value: CanvasPattern): CanvasPattern;\nexport function color(\n value:\n | string\n | { r: number; g: number; b: number; a: number }\n | [number, number, number]\n | [number, number, number, number]\n): Color;\nexport function color(value) {\n return isPatternOrGradient(value) ? value : new Color(value);\n}\n\nexport function getHoverColor(value: CanvasGradient): CanvasGradient;\nexport function getHoverColor(value: CanvasPattern): CanvasPattern;\nexport function getHoverColor(value: string): string;\nexport function getHoverColor(value) {\n return isPatternOrGradient(value)\n ? value\n : new Color(value).saturate(0.5).darken(0.1).hexString();\n}\n","const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension'];\nconst colors = ['color', 'borderColor', 'backgroundColor'];\n\nexport function applyAnimationsDefaults(defaults) {\n defaults.set('animation', {\n delay: undefined,\n duration: 1000,\n easing: 'easeOutQuart',\n fn: undefined,\n from: undefined,\n loop: undefined,\n to: undefined,\n type: undefined,\n });\n\n defaults.describe('animation', {\n _fallback: false,\n _indexable: false,\n _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn',\n });\n\n defaults.set('animations', {\n colors: {\n type: 'color',\n properties: colors\n },\n numbers: {\n type: 'number',\n properties: numbers\n },\n });\n\n defaults.describe('animations', {\n _fallback: 'animation',\n });\n\n defaults.set('transitions', {\n active: {\n animation: {\n duration: 400\n }\n },\n resize: {\n animation: {\n duration: 0\n }\n },\n show: {\n animations: {\n colors: {\n from: 'transparent'\n },\n visible: {\n type: 'boolean',\n duration: 0 // show immediately\n },\n }\n },\n hide: {\n animations: {\n colors: {\n to: 'transparent'\n },\n visible: {\n type: 'boolean',\n easing: 'linear',\n fn: v => v | 0 // for keeping the dataset visible all the way through the animation\n },\n }\n }\n });\n}\n","\nconst intlCache = new Map();\n\nfunction getNumberFormat(locale: string, options?: Intl.NumberFormatOptions) {\n options = options || {};\n const cacheKey = locale + JSON.stringify(options);\n let formatter = intlCache.get(cacheKey);\n if (!formatter) {\n formatter = new Intl.NumberFormat(locale, options);\n intlCache.set(cacheKey, formatter);\n }\n return formatter;\n}\n\nexport function formatNumber(num: number, locale: string, options?: Intl.NumberFormatOptions) {\n return getNumberFormat(locale, options).format(num);\n}\n","import {isArray} from '../helpers/helpers.core.js';\nimport {formatNumber} from '../helpers/helpers.intl.js';\nimport {log10} from '../helpers/helpers.math.js';\n\n/**\n * Namespace to hold formatters for different types of ticks\n * @namespace Chart.Ticks.formatters\n */\nconst formatters = {\n /**\n * Formatter for value labels\n * @method Chart.Ticks.formatters.values\n * @param value the value to display\n * @return {string|string[]} the label to display\n */\n values(value) {\n return isArray(value) ? /** @type {string[]} */ (value) : '' + value;\n },\n\n /**\n * Formatter for numeric ticks\n * @method Chart.Ticks.formatters.numeric\n * @param tickValue {number} the value to be formatted\n * @param index {number} the position of the tickValue parameter in the ticks array\n * @param ticks {object[]} the list of ticks being converted\n * @return {string} string representation of the tickValue parameter\n */\n numeric(tickValue, index, ticks) {\n if (tickValue === 0) {\n return '0'; // never show decimal places for 0\n }\n\n const locale = this.chart.options.locale;\n let notation;\n let delta = tickValue; // This is used when there are less than 2 ticks as the tick interval.\n\n if (ticks.length > 1) {\n // all ticks are small or there huge numbers; use scientific notation\n const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value));\n if (maxTick < 1e-4 || maxTick > 1e+15) {\n notation = 'scientific';\n }\n\n delta = calculateDelta(tickValue, ticks);\n }\n\n const logDelta = log10(Math.abs(delta));\n\n // When datasets have values approaching Number.MAX_VALUE, the tick calculations might result in\n // infinity and eventually NaN. Passing NaN for minimumFractionDigits or maximumFractionDigits\n // will make the number formatter throw. So instead we check for isNaN and use a fallback value.\n //\n // toFixed has a max of 20 decimal places\n const numDecimal = isNaN(logDelta) ? 1 : Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0);\n\n const options = {notation, minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal};\n Object.assign(options, this.options.ticks.format);\n\n return formatNumber(tickValue, locale, options);\n },\n\n\n /**\n * Formatter for logarithmic ticks\n * @method Chart.Ticks.formatters.logarithmic\n * @param tickValue {number} the value to be formatted\n * @param index {number} the position of the tickValue parameter in the ticks array\n * @param ticks {object[]} the list of ticks being converted\n * @return {string} string representation of the tickValue parameter\n */\n logarithmic(tickValue, index, ticks) {\n if (tickValue === 0) {\n return '0';\n }\n const remain = ticks[index].significand || (tickValue / (Math.pow(10, Math.floor(log10(tickValue)))));\n if ([1, 2, 3, 5, 10, 15].includes(remain) || index > 0.8 * ticks.length) {\n return formatters.numeric.call(this, tickValue, index, ticks);\n }\n return '';\n }\n\n};\n\n\nfunction calculateDelta(tickValue, ticks) {\n // Figure out how many digits to show\n // The space between the first two ticks might be smaller than normal spacing\n let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value;\n\n // If we have a number like 2.5 as the delta, figure out how many decimal places we need\n if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) {\n // not an integer\n delta = tickValue - Math.floor(tickValue);\n }\n return delta;\n}\n\n/**\n * Namespace to hold static tick generation functions\n * @namespace Chart.Ticks\n */\nexport default {formatters};\n","import {getHoverColor} from '../helpers/helpers.color.js';\nimport {isObject, merge, valueOrDefault} from '../helpers/helpers.core.js';\nimport {applyAnimationsDefaults} from './core.animations.defaults.js';\nimport {applyLayoutsDefaults} from './core.layouts.defaults.js';\nimport {applyScaleDefaults} from './core.scale.defaults.js';\n\nexport const overrides = Object.create(null);\nexport const descriptors = Object.create(null);\n\n/**\n * @param {object} node\n * @param {string} key\n * @return {object}\n */\nfunction getScope(node, key) {\n if (!key) {\n return node;\n }\n const keys = key.split('.');\n for (let i = 0, n = keys.length; i < n; ++i) {\n const k = keys[i];\n node = node[k] || (node[k] = Object.create(null));\n }\n return node;\n}\n\nfunction set(root, scope, values) {\n if (typeof scope === 'string') {\n return merge(getScope(root, scope), values);\n }\n return merge(getScope(root, ''), scope);\n}\n\n/**\n * Please use the module's default export which provides a singleton instance\n * Note: class is exported for typedoc\n */\nexport class Defaults {\n constructor(_descriptors, _appliers) {\n this.animation = undefined;\n this.backgroundColor = 'rgba(0,0,0,0.1)';\n this.borderColor = 'rgba(0,0,0,0.1)';\n this.color = '#666';\n this.datasets = {};\n this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio();\n this.elements = {};\n this.events = [\n 'mousemove',\n 'mouseout',\n 'click',\n 'touchstart',\n 'touchmove'\n ];\n this.font = {\n family: \"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif\",\n size: 12,\n style: 'normal',\n lineHeight: 1.2,\n weight: null\n };\n this.hover = {};\n this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor);\n this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor);\n this.hoverColor = (ctx, options) => getHoverColor(options.color);\n this.indexAxis = 'x';\n this.interaction = {\n mode: 'nearest',\n intersect: true,\n includeInvisible: false\n };\n this.maintainAspectRatio = true;\n this.onHover = null;\n this.onClick = null;\n this.parsing = true;\n this.plugins = {};\n this.responsive = true;\n this.scale = undefined;\n this.scales = {};\n this.showLine = true;\n this.drawActiveElementsOnTop = true;\n\n this.describe(_descriptors);\n this.apply(_appliers);\n }\n\n /**\n\t * @param {string|object} scope\n\t * @param {object} [values]\n\t */\n set(scope, values) {\n return set(this, scope, values);\n }\n\n /**\n\t * @param {string} scope\n\t */\n get(scope) {\n return getScope(this, scope);\n }\n\n /**\n\t * @param {string|object} scope\n\t * @param {object} [values]\n\t */\n describe(scope, values) {\n return set(descriptors, scope, values);\n }\n\n override(scope, values) {\n return set(overrides, scope, values);\n }\n\n /**\n\t * Routes the named defaults to fallback to another scope/name.\n\t * This routing is useful when those target values, like defaults.color, are changed runtime.\n\t * If the values would be copied, the runtime change would not take effect. By routing, the\n\t * fallback is evaluated at each access, so its always up to date.\n\t *\n\t * Example:\n\t *\n\t * \tdefaults.route('elements.arc', 'backgroundColor', '', 'color')\n\t * - reads the backgroundColor from defaults.color when undefined locally\n\t *\n\t * @param {string} scope Scope this route applies to.\n\t * @param {string} name Property name that should be routed to different namespace when not defined here.\n\t * @param {string} targetScope The namespace where those properties should be routed to.\n\t * Empty string ('') is the root of defaults.\n\t * @param {string} targetName The target name in the target scope the property should be routed to.\n\t */\n route(scope, name, targetScope, targetName) {\n const scopeObject = getScope(this, scope);\n const targetScopeObject = getScope(this, targetScope);\n const privateName = '_' + name;\n\n Object.defineProperties(scopeObject, {\n // A private property is defined to hold the actual value, when this property is set in its scope (set in the setter)\n [privateName]: {\n value: scopeObject[name],\n writable: true\n },\n // The actual property is defined as getter/setter so we can do the routing when value is not locally set.\n [name]: {\n enumerable: true,\n get() {\n const local = this[privateName];\n const target = targetScopeObject[targetName];\n if (isObject(local)) {\n return Object.assign({}, target, local);\n }\n return valueOrDefault(local, target);\n },\n set(value) {\n this[privateName] = value;\n }\n }\n });\n }\n\n apply(appliers) {\n appliers.forEach((apply) => apply(this));\n }\n}\n\n// singleton instance\nexport default /* #__PURE__ */ new Defaults({\n _scriptable: (name) => !name.startsWith('on'),\n _indexable: (name) => name !== 'events',\n hover: {\n _fallback: 'interaction'\n },\n interaction: {\n _scriptable: false,\n _indexable: false,\n }\n}, [applyAnimationsDefaults, applyLayoutsDefaults, applyScaleDefaults]);\n","export function applyLayoutsDefaults(defaults) {\n defaults.set('layout', {\n autoPadding: true,\n padding: {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n }\n });\n}\n","import Ticks from './core.ticks.js';\n\nexport function applyScaleDefaults(defaults) {\n defaults.set('scale', {\n display: true,\n offset: false,\n reverse: false,\n beginAtZero: false,\n\n /**\n * Scale boundary strategy (bypassed by min/max time options)\n * - `data`: make sure data are fully visible, ticks outside are removed\n * - `ticks`: make sure ticks are fully visible, data outside are truncated\n * @see https://github.com/chartjs/Chart.js/pull/4556\n * @since 3.0.0\n */\n bounds: 'ticks',\n\n clip: true,\n\n /**\n * Addition grace added to max and reduced from min data value.\n * @since 3.0.0\n */\n grace: 0,\n\n // grid line settings\n grid: {\n display: true,\n lineWidth: 1,\n drawOnChartArea: true,\n drawTicks: true,\n tickLength: 8,\n tickWidth: (_ctx, options) => options.lineWidth,\n tickColor: (_ctx, options) => options.color,\n offset: false,\n },\n\n border: {\n display: true,\n dash: [],\n dashOffset: 0.0,\n width: 1\n },\n\n // scale title\n title: {\n // display property\n display: false,\n\n // actual label\n text: '',\n\n // top/bottom padding\n padding: {\n top: 4,\n bottom: 4\n }\n },\n\n // label settings\n ticks: {\n minRotation: 0,\n maxRotation: 50,\n mirror: false,\n textStrokeWidth: 0,\n textStrokeColor: '',\n padding: 3,\n display: true,\n autoSkip: true,\n autoSkipPadding: 3,\n labelOffset: 0,\n // We pass through arrays to be rendered as multiline labels, we convert Others to strings here.\n callback: Ticks.formatters.values,\n minor: {},\n major: {},\n align: 'center',\n crossAlign: 'near',\n\n showLabelBackdrop: false,\n backdropColor: 'rgba(255, 255, 255, 0.75)',\n backdropPadding: 2,\n }\n });\n\n defaults.route('scale.ticks', 'color', '', 'color');\n defaults.route('scale.grid', 'color', '', 'borderColor');\n defaults.route('scale.border', 'color', '', 'borderColor');\n defaults.route('scale.title', 'color', '', 'color');\n\n defaults.describe('scale', {\n _fallback: false,\n _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser',\n _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash' && name !== 'dash',\n });\n\n defaults.describe('scales', {\n _fallback: 'scale',\n });\n\n defaults.describe('scale.ticks', {\n _scriptable: (name) => name !== 'backdropPadding' && name !== 'callback',\n _indexable: (name) => name !== 'backdropPadding',\n });\n}\n","import type {ChartArea, Scale} from '../types/index.js';\nimport type Chart from '../core/core.controller.js';\nimport type {ChartEvent} from '../types.js';\nimport {INFINITY} from './helpers.math.js';\n\n/**\n * Note: typedefs are auto-exported, so use a made-up `dom` namespace where\n * necessary to avoid duplicates with `export * from './helpers`; see\n * https://github.com/microsoft/TypeScript/issues/46011\n * @typedef { import('../core/core.controller.js').default } dom.Chart\n * @typedef { import('../../types').ChartEvent } ChartEvent\n */\n\n/**\n * @private\n */\nexport function _isDomSupported(): boolean {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n}\n\n/**\n * @private\n */\nexport function _getParentNode(domNode: HTMLCanvasElement): HTMLCanvasElement {\n let parent = domNode.parentNode;\n if (parent && parent.toString() === '[object ShadowRoot]') {\n parent = (parent as ShadowRoot).host;\n }\n return parent as HTMLCanvasElement;\n}\n\n/**\n * convert max-width/max-height values that may be percentages into a number\n * @private\n */\n\nfunction parseMaxStyle(styleValue: string | number, node: HTMLElement, parentProperty: string) {\n let valueInPixels: number;\n if (typeof styleValue === 'string') {\n valueInPixels = parseInt(styleValue, 10);\n\n if (styleValue.indexOf('%') !== -1) {\n // percentage * size in dimension\n valueInPixels = (valueInPixels / 100) * node.parentNode[parentProperty];\n }\n } else {\n valueInPixels = styleValue;\n }\n\n return valueInPixels;\n}\n\nconst getComputedStyle = (element: HTMLElement): CSSStyleDeclaration =>\n element.ownerDocument.defaultView.getComputedStyle(element, null);\n\nexport function getStyle(el: HTMLElement, property: string): string {\n return getComputedStyle(el).getPropertyValue(property);\n}\n\nconst positions = ['top', 'right', 'bottom', 'left'];\nfunction getPositionedStyle(styles: CSSStyleDeclaration, style: string, suffix?: string): ChartArea {\n const result = {} as ChartArea;\n suffix = suffix ? '-' + suffix : '';\n for (let i = 0; i < 4; i++) {\n const pos = positions[i];\n result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0;\n }\n result.width = result.left + result.right;\n result.height = result.top + result.bottom;\n return result;\n}\n\nconst useOffsetPos = (x: number, y: number, target: HTMLElement | EventTarget) =>\n (x > 0 || y > 0) && (!target || !(target as HTMLElement).shadowRoot);\n\n/**\n * @param e\n * @param canvas\n * @returns Canvas position\n */\nfunction getCanvasPosition(\n e: Event | TouchEvent | MouseEvent,\n canvas: HTMLCanvasElement\n): {\n x: number;\n y: number;\n box: boolean;\n } {\n const touches = (e as TouchEvent).touches;\n const source = (touches && touches.length ? touches[0] : e) as MouseEvent;\n const {offsetX, offsetY} = source as MouseEvent;\n let box = false;\n let x, y;\n if (useOffsetPos(offsetX, offsetY, e.target)) {\n x = offsetX;\n y = offsetY;\n } else {\n const rect = canvas.getBoundingClientRect();\n x = source.clientX - rect.left;\n y = source.clientY - rect.top;\n box = true;\n }\n return {x, y, box};\n}\n\n/**\n * Gets an event's x, y coordinates, relative to the chart area\n * @param event\n * @param chart\n * @returns x and y coordinates of the event\n */\n\nexport function getRelativePosition(\n event: Event | ChartEvent | TouchEvent | MouseEvent,\n chart: Chart\n): { x: number; y: number } {\n if ('native' in event) {\n return event;\n }\n\n const {canvas, currentDevicePixelRatio} = chart;\n const style = getComputedStyle(canvas);\n const borderBox = style.boxSizing === 'border-box';\n const paddings = getPositionedStyle(style, 'padding');\n const borders = getPositionedStyle(style, 'border', 'width');\n const {x, y, box} = getCanvasPosition(event, canvas);\n const xOffset = paddings.left + (box && borders.left);\n const yOffset = paddings.top + (box && borders.top);\n\n let {width, height} = chart;\n if (borderBox) {\n width -= paddings.width + borders.width;\n height -= paddings.height + borders.height;\n }\n return {\n x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio),\n y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio)\n };\n}\n\nfunction getContainerSize(canvas: HTMLCanvasElement, width: number, height: number): Partial {\n let maxWidth: number, maxHeight: number;\n\n if (width === undefined || height === undefined) {\n const container = canvas && _getParentNode(canvas);\n if (!container) {\n width = canvas.clientWidth;\n height = canvas.clientHeight;\n } else {\n const rect = container.getBoundingClientRect(); // this is the border box of the container\n const containerStyle = getComputedStyle(container);\n const containerBorder = getPositionedStyle(containerStyle, 'border', 'width');\n const containerPadding = getPositionedStyle(containerStyle, 'padding');\n width = rect.width - containerPadding.width - containerBorder.width;\n height = rect.height - containerPadding.height - containerBorder.height;\n maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth');\n maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight');\n }\n }\n return {\n width,\n height,\n maxWidth: maxWidth || INFINITY,\n maxHeight: maxHeight || INFINITY\n };\n}\n\nconst round1 = (v: number) => Math.round(v * 10) / 10;\n\n// eslint-disable-next-line complexity\nexport function getMaximumSize(\n canvas: HTMLCanvasElement,\n bbWidth?: number,\n bbHeight?: number,\n aspectRatio?: number\n): { width: number; height: number } {\n const style = getComputedStyle(canvas);\n const margins = getPositionedStyle(style, 'margin');\n const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY;\n const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY;\n const containerSize = getContainerSize(canvas, bbWidth, bbHeight);\n let {width, height} = containerSize;\n\n if (style.boxSizing === 'content-box') {\n const borders = getPositionedStyle(style, 'border', 'width');\n const paddings = getPositionedStyle(style, 'padding');\n width -= paddings.width + borders.width;\n height -= paddings.height + borders.height;\n }\n width = Math.max(0, width - margins.width);\n height = Math.max(0, aspectRatio ? width / aspectRatio : height - margins.height);\n width = round1(Math.min(width, maxWidth, containerSize.maxWidth));\n height = round1(Math.min(height, maxHeight, containerSize.maxHeight));\n if (width && !height) {\n // https://github.com/chartjs/Chart.js/issues/4659\n // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default)\n height = round1(width / 2);\n }\n\n const maintainHeight = bbWidth !== undefined || bbHeight !== undefined;\n\n if (maintainHeight && aspectRatio && containerSize.height && height > containerSize.height) {\n height = containerSize.height;\n width = round1(Math.floor(height * aspectRatio));\n }\n\n return {width, height};\n}\n\n/**\n * @param chart\n * @param forceRatio\n * @param forceStyle\n * @returns True if the canvas context size or transformation has changed.\n */\nexport function retinaScale(\n chart: Chart,\n forceRatio: number,\n forceStyle?: boolean\n): boolean | void {\n const pixelRatio = forceRatio || 1;\n const deviceHeight = Math.floor(chart.height * pixelRatio);\n const deviceWidth = Math.floor(chart.width * pixelRatio);\n\n chart.height = Math.floor(chart.height);\n chart.width = Math.floor(chart.width);\n\n const canvas = chart.canvas;\n\n // If no style has been set on the canvas, the render size is used as display size,\n // making the chart visually bigger, so let's enforce it to the \"correct\" values.\n // See https://github.com/chartjs/Chart.js/issues/3575\n if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) {\n canvas.style.height = `${chart.height}px`;\n canvas.style.width = `${chart.width}px`;\n }\n\n if (chart.currentDevicePixelRatio !== pixelRatio\n || canvas.height !== deviceHeight\n || canvas.width !== deviceWidth) {\n chart.currentDevicePixelRatio = pixelRatio;\n canvas.height = deviceHeight;\n canvas.width = deviceWidth;\n chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);\n return true;\n }\n return false;\n}\n\n/**\n * Detects support for options object argument in addEventListener.\n * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support\n * @private\n */\nexport const supportsEventListenerOptions = (function() {\n let passiveSupported = false;\n try {\n const options = {\n get passive() { // This function will be called when the browser attempts to access the passive property.\n passiveSupported = true;\n return false;\n }\n } as EventListenerOptions;\n\n if (_isDomSupported()) {\n window.addEventListener('test', null, options);\n window.removeEventListener('test', null, options);\n }\n } catch (e) {\n // continue regardless of error\n }\n return passiveSupported;\n}());\n\n/**\n * The \"used\" size is the final value of a dimension property after all calculations have\n * been performed. This method uses the computed style of `element` but returns undefined\n * if the computed style is not expressed in pixels. That can happen in some cases where\n * `element` has a size relative to its parent and this last one is not yet displayed,\n * for example because of `display: none` on a parent node.\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value\n * @returns Size in pixels or undefined if unknown.\n */\n\nexport function readUsedSize(\n element: HTMLElement,\n property: 'width' | 'height'\n): number | undefined {\n const value = getStyle(element, property);\n const matches = value && value.match(/^(\\d+)(\\.\\d+)?px$/);\n return matches ? +matches[1] : undefined;\n}\n","import type {\n Chart,\n Point,\n FontSpec,\n CanvasFontSpec,\n PointStyle,\n RenderTextOpts,\n BackdropOptions\n} from '../types/index.js';\nimport type {\n TRBL,\n SplinePoint,\n RoundedRect,\n TRBLCorners\n} from '../types/geometric.js';\nimport {isArray, isNullOrUndef} from './helpers.core.js';\nimport {PI, TAU, HALF_PI, QUARTER_PI, TWO_THIRDS_PI, RAD_PER_DEG} from './helpers.math.js';\n\n/**\n * Converts the given font object into a CSS font string.\n * @param font - A font object.\n * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font\n * @private\n */\nexport function toFontString(font: FontSpec) {\n if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) {\n return null;\n }\n\n return (font.style ? font.style + ' ' : '')\n\t\t+ (font.weight ? font.weight + ' ' : '')\n\t\t+ font.size + 'px '\n\t\t+ font.family;\n}\n\n/**\n * @private\n */\nexport function _measureText(\n ctx: CanvasRenderingContext2D,\n data: Record,\n gc: string[],\n longest: number,\n string: string\n) {\n let textWidth = data[string];\n if (!textWidth) {\n textWidth = data[string] = ctx.measureText(string).width;\n gc.push(string);\n }\n if (textWidth > longest) {\n longest = textWidth;\n }\n return longest;\n}\n\ntype Thing = string | undefined | null\ntype Things = (Thing | Thing[])[]\n\n/**\n * @private\n */\n// eslint-disable-next-line complexity\nexport function _longestText(\n ctx: CanvasRenderingContext2D,\n font: string,\n arrayOfThings: Things,\n cache?: {data?: Record, garbageCollect?: string[], font?: string}\n) {\n cache = cache || {};\n let data = cache.data = cache.data || {};\n let gc = cache.garbageCollect = cache.garbageCollect || [];\n\n if (cache.font !== font) {\n data = cache.data = {};\n gc = cache.garbageCollect = [];\n cache.font = font;\n }\n\n ctx.save();\n\n ctx.font = font;\n let longest = 0;\n const ilen = arrayOfThings.length;\n let i: number, j: number, jlen: number, thing: Thing | Thing[], nestedThing: Thing | Thing[];\n for (i = 0; i < ilen; i++) {\n thing = arrayOfThings[i];\n\n // Undefined strings and arrays should not be measured\n if (thing !== undefined && thing !== null && !isArray(thing)) {\n longest = _measureText(ctx, data, gc, longest, thing);\n } else if (isArray(thing)) {\n // if it is an array lets measure each element\n // to do maybe simplify this function a bit so we can do this more recursively?\n for (j = 0, jlen = thing.length; j < jlen; j++) {\n nestedThing = thing[j];\n // Undefined strings and arrays should not be measured\n if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) {\n longest = _measureText(ctx, data, gc, longest, nestedThing);\n }\n }\n }\n }\n\n ctx.restore();\n\n const gcLen = gc.length / 2;\n if (gcLen > arrayOfThings.length) {\n for (i = 0; i < gcLen; i++) {\n delete data[gc[i]];\n }\n gc.splice(0, gcLen);\n }\n return longest;\n}\n\n/**\n * Returns the aligned pixel value to avoid anti-aliasing blur\n * @param chart - The chart instance.\n * @param pixel - A pixel value.\n * @param width - The width of the element.\n * @returns The aligned pixel value.\n * @private\n */\nexport function _alignPixel(chart: Chart, pixel: number, width: number) {\n const devicePixelRatio = chart.currentDevicePixelRatio;\n const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0;\n return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth;\n}\n\n/**\n * Clears the entire canvas.\n */\nexport function clearCanvas(canvas?: HTMLCanvasElement, ctx?: CanvasRenderingContext2D) {\n if (!ctx && !canvas) {\n return;\n }\n\n ctx = ctx || canvas.getContext('2d');\n\n ctx.save();\n // canvas.width and canvas.height do not consider the canvas transform,\n // while clearRect does\n ctx.resetTransform();\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.restore();\n}\n\nexport interface DrawPointOptions {\n pointStyle: PointStyle;\n rotation?: number;\n radius: number;\n borderWidth: number;\n}\n\nexport function drawPoint(\n ctx: CanvasRenderingContext2D,\n options: DrawPointOptions,\n x: number,\n y: number\n) {\n // eslint-disable-next-line @typescript-eslint/no-use-before-define\n drawPointLegend(ctx, options, x, y, null);\n}\n\n// eslint-disable-next-line complexity\nexport function drawPointLegend(\n ctx: CanvasRenderingContext2D,\n options: DrawPointOptions,\n x: number,\n y: number,\n w: number\n) {\n let type: string, xOffset: number, yOffset: number, size: number, cornerRadius: number, width: number, xOffsetW: number, yOffsetW: number;\n const style = options.pointStyle;\n const rotation = options.rotation;\n const radius = options.radius;\n let rad = (rotation || 0) * RAD_PER_DEG;\n\n if (style && typeof style === 'object') {\n type = style.toString();\n if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') {\n ctx.save();\n ctx.translate(x, y);\n ctx.rotate(rad);\n ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height);\n ctx.restore();\n return;\n }\n }\n\n if (isNaN(radius) || radius <= 0) {\n return;\n }\n\n ctx.beginPath();\n\n switch (style) {\n // Default includes circle\n default:\n if (w) {\n ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU);\n } else {\n ctx.arc(x, y, radius, 0, TAU);\n }\n ctx.closePath();\n break;\n case 'triangle':\n width = w ? w / 2 : radius;\n ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);\n rad += TWO_THIRDS_PI;\n ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);\n rad += TWO_THIRDS_PI;\n ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius);\n ctx.closePath();\n break;\n case 'rectRounded':\n // NOTE: the rounded rect implementation changed to use `arc` instead of\n // `quadraticCurveTo` since it generates better results when rect is\n // almost a circle. 0.516 (instead of 0.5) produces results with visually\n // closer proportion to the previous impl and it is inscribed in the\n // circle with `radius`. For more details, see the following PRs:\n // https://github.com/chartjs/Chart.js/issues/5597\n // https://github.com/chartjs/Chart.js/issues/5858\n cornerRadius = radius * 0.516;\n size = radius - cornerRadius;\n xOffset = Math.cos(rad + QUARTER_PI) * size;\n xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);\n yOffset = Math.sin(rad + QUARTER_PI) * size;\n yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size);\n ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI);\n ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad);\n ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI);\n ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI);\n ctx.closePath();\n break;\n case 'rect':\n if (!rotation) {\n size = Math.SQRT1_2 * radius;\n width = w ? w / 2 : size;\n ctx.rect(x - width, y - size, 2 * width, 2 * size);\n break;\n }\n rad += QUARTER_PI;\n /* falls through */\n case 'rectRot':\n xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);\n xOffset = Math.cos(rad) * radius;\n yOffset = Math.sin(rad) * radius;\n yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);\n ctx.moveTo(x - xOffsetW, y - yOffset);\n ctx.lineTo(x + yOffsetW, y - xOffset);\n ctx.lineTo(x + xOffsetW, y + yOffset);\n ctx.lineTo(x - yOffsetW, y + xOffset);\n ctx.closePath();\n break;\n case 'crossRot':\n rad += QUARTER_PI;\n /* falls through */\n case 'cross':\n xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);\n xOffset = Math.cos(rad) * radius;\n yOffset = Math.sin(rad) * radius;\n yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);\n ctx.moveTo(x - xOffsetW, y - yOffset);\n ctx.lineTo(x + xOffsetW, y + yOffset);\n ctx.moveTo(x + yOffsetW, y - xOffset);\n ctx.lineTo(x - yOffsetW, y + xOffset);\n break;\n case 'star':\n xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);\n xOffset = Math.cos(rad) * radius;\n yOffset = Math.sin(rad) * radius;\n yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);\n ctx.moveTo(x - xOffsetW, y - yOffset);\n ctx.lineTo(x + xOffsetW, y + yOffset);\n ctx.moveTo(x + yOffsetW, y - xOffset);\n ctx.lineTo(x - yOffsetW, y + xOffset);\n rad += QUARTER_PI;\n xOffsetW = Math.cos(rad) * (w ? w / 2 : radius);\n xOffset = Math.cos(rad) * radius;\n yOffset = Math.sin(rad) * radius;\n yOffsetW = Math.sin(rad) * (w ? w / 2 : radius);\n ctx.moveTo(x - xOffsetW, y - yOffset);\n ctx.lineTo(x + xOffsetW, y + yOffset);\n ctx.moveTo(x + yOffsetW, y - xOffset);\n ctx.lineTo(x - yOffsetW, y + xOffset);\n break;\n case 'line':\n xOffset = w ? w / 2 : Math.cos(rad) * radius;\n yOffset = Math.sin(rad) * radius;\n ctx.moveTo(x - xOffset, y - yOffset);\n ctx.lineTo(x + xOffset, y + yOffset);\n break;\n case 'dash':\n ctx.moveTo(x, y);\n ctx.lineTo(x + Math.cos(rad) * (w ? w / 2 : radius), y + Math.sin(rad) * radius);\n break;\n case false:\n ctx.closePath();\n break;\n }\n\n ctx.fill();\n if (options.borderWidth > 0) {\n ctx.stroke();\n }\n}\n\n/**\n * Returns true if the point is inside the rectangle\n * @param point - The point to test\n * @param area - The rectangle\n * @param margin - allowed margin\n * @private\n */\nexport function _isPointInArea(\n point: Point,\n area: TRBL,\n margin?: number\n) {\n margin = margin || 0.5; // margin - default is to match rounded decimals\n\n return !area || (point && point.x > area.left - margin && point.x < area.right + margin &&\n\t\tpoint.y > area.top - margin && point.y < area.bottom + margin);\n}\n\nexport function clipArea(ctx: CanvasRenderingContext2D, area: TRBL) {\n ctx.save();\n ctx.beginPath();\n ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top);\n ctx.clip();\n}\n\nexport function unclipArea(ctx: CanvasRenderingContext2D) {\n ctx.restore();\n}\n\n/**\n * @private\n */\nexport function _steppedLineTo(\n ctx: CanvasRenderingContext2D,\n previous: Point,\n target: Point,\n flip?: boolean,\n mode?: string\n) {\n if (!previous) {\n return ctx.lineTo(target.x, target.y);\n }\n if (mode === 'middle') {\n const midpoint = (previous.x + target.x) / 2.0;\n ctx.lineTo(midpoint, previous.y);\n ctx.lineTo(midpoint, target.y);\n } else if (mode === 'after' !== !!flip) {\n ctx.lineTo(previous.x, target.y);\n } else {\n ctx.lineTo(target.x, previous.y);\n }\n ctx.lineTo(target.x, target.y);\n}\n\n/**\n * @private\n */\nexport function _bezierCurveTo(\n ctx: CanvasRenderingContext2D,\n previous: SplinePoint,\n target: SplinePoint,\n flip?: boolean\n) {\n if (!previous) {\n return ctx.lineTo(target.x, target.y);\n }\n ctx.bezierCurveTo(\n flip ? previous.cp1x : previous.cp2x,\n flip ? previous.cp1y : previous.cp2y,\n flip ? target.cp2x : target.cp1x,\n flip ? target.cp2y : target.cp1y,\n target.x,\n target.y);\n}\n\nfunction setRenderOpts(ctx: CanvasRenderingContext2D, opts: RenderTextOpts) {\n if (opts.translation) {\n ctx.translate(opts.translation[0], opts.translation[1]);\n }\n\n if (!isNullOrUndef(opts.rotation)) {\n ctx.rotate(opts.rotation);\n }\n\n if (opts.color) {\n ctx.fillStyle = opts.color;\n }\n\n if (opts.textAlign) {\n ctx.textAlign = opts.textAlign;\n }\n\n if (opts.textBaseline) {\n ctx.textBaseline = opts.textBaseline;\n }\n}\n\nfunction decorateText(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n line: string,\n opts: RenderTextOpts\n) {\n if (opts.strikethrough || opts.underline) {\n /**\n * Now that IE11 support has been dropped, we can use more\n * of the TextMetrics object. The actual bounding boxes\n * are unflagged in Chrome, Firefox, Edge, and Safari so they\n * can be safely used.\n * See https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Browser_compatibility\n */\n const metrics = ctx.measureText(line);\n const left = x - metrics.actualBoundingBoxLeft;\n const right = x + metrics.actualBoundingBoxRight;\n const top = y - metrics.actualBoundingBoxAscent;\n const bottom = y + metrics.actualBoundingBoxDescent;\n const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom;\n\n ctx.strokeStyle = ctx.fillStyle;\n ctx.beginPath();\n ctx.lineWidth = opts.decorationWidth || 2;\n ctx.moveTo(left, yDecoration);\n ctx.lineTo(right, yDecoration);\n ctx.stroke();\n }\n}\n\nfunction drawBackdrop(ctx: CanvasRenderingContext2D, opts: BackdropOptions) {\n const oldColor = ctx.fillStyle;\n\n ctx.fillStyle = opts.color as string;\n ctx.fillRect(opts.left, opts.top, opts.width, opts.height);\n ctx.fillStyle = oldColor;\n}\n\n/**\n * Render text onto the canvas\n */\nexport function renderText(\n ctx: CanvasRenderingContext2D,\n text: string | string[],\n x: number,\n y: number,\n font: CanvasFontSpec,\n opts: RenderTextOpts = {}\n) {\n const lines = isArray(text) ? text : [text];\n const stroke = opts.strokeWidth > 0 && opts.strokeColor !== '';\n let i: number, line: string;\n\n ctx.save();\n ctx.font = font.string;\n setRenderOpts(ctx, opts);\n\n for (i = 0; i < lines.length; ++i) {\n line = lines[i];\n\n if (opts.backdrop) {\n drawBackdrop(ctx, opts.backdrop);\n }\n\n if (stroke) {\n if (opts.strokeColor) {\n ctx.strokeStyle = opts.strokeColor;\n }\n\n if (!isNullOrUndef(opts.strokeWidth)) {\n ctx.lineWidth = opts.strokeWidth;\n }\n\n ctx.strokeText(line, x, y, opts.maxWidth);\n }\n\n ctx.fillText(line, x, y, opts.maxWidth);\n decorateText(ctx, x, y, line, opts);\n\n y += Number(font.lineHeight);\n }\n\n ctx.restore();\n}\n\n/**\n * Add a path of a rectangle with rounded corners to the current sub-path\n * @param ctx - Context\n * @param rect - Bounding rect\n */\nexport function addRoundedRectPath(\n ctx: CanvasRenderingContext2D,\n rect: RoundedRect & { radius: TRBLCorners }\n) {\n const {x, y, w, h, radius} = rect;\n\n // top left arc\n ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, 1.5 * PI, PI, true);\n\n // line from top left to bottom left\n ctx.lineTo(x, y + h - radius.bottomLeft);\n\n // bottom left arc\n ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);\n\n // line from bottom left to bottom right\n ctx.lineTo(x + w - radius.bottomRight, y + h);\n\n // bottom right arc\n ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);\n\n // line from bottom right to top right\n ctx.lineTo(x + w, y + radius.topRight);\n\n // top right arc\n ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);\n\n // line from top right to top left\n ctx.lineTo(x + radius.topLeft, y);\n}\n","/* eslint-disable @typescript-eslint/no-use-before-define */\nimport type {AnyObject} from '../types/basic.js';\nimport type {ChartMeta} from '../types/index.js';\nimport type {\n ResolverObjectKey,\n ResolverCache,\n ResolverProxy,\n DescriptorDefaults,\n Descriptor,\n ContextCache,\n ContextProxy\n} from './helpers.config.types.js';\nimport {isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core.js';\n\nexport * from './helpers.config.types.js';\n\n/**\n * Creates a Proxy for resolving raw values for options.\n * @param scopes - The option scopes to look for values, in resolution order\n * @param prefixes - The prefixes for values, in resolution order.\n * @param rootScopes - The root option scopes\n * @param fallback - Parent scopes fallback\n * @param getTarget - callback for getting the target for changed values\n * @returns Proxy\n * @private\n */\nexport function _createResolver<\n T extends AnyObject[] = AnyObject[],\n R extends AnyObject[] = T\n>(\n scopes: T,\n prefixes = [''],\n rootScopes?: R,\n fallback?: ResolverObjectKey,\n getTarget = () => scopes[0]\n) {\n const finalRootScopes = rootScopes || scopes;\n if (typeof fallback === 'undefined') {\n fallback = _resolve('_fallback', scopes);\n }\n const cache: ResolverCache = {\n [Symbol.toStringTag]: 'Object',\n _cacheable: true,\n _scopes: scopes,\n _rootScopes: finalRootScopes,\n _fallback: fallback,\n _getTarget: getTarget,\n override: (scope: AnyObject) => _createResolver([scope, ...scopes], prefixes, finalRootScopes, fallback),\n };\n return new Proxy(cache, {\n /**\n * A trap for the delete operator.\n */\n deleteProperty(target, prop: string) {\n delete target[prop]; // remove from cache\n delete target._keys; // remove cached keys\n delete scopes[0][prop]; // remove from top level scope\n return true;\n },\n\n /**\n * A trap for getting property values.\n */\n get(target, prop: string) {\n return _cached(target, prop,\n () => _resolveWithPrefixes(prop, prefixes, scopes, target));\n },\n\n /**\n * A trap for Object.getOwnPropertyDescriptor.\n * Also used by Object.hasOwnProperty.\n */\n getOwnPropertyDescriptor(target, prop) {\n return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop);\n },\n\n /**\n * A trap for Object.getPrototypeOf.\n */\n getPrototypeOf() {\n return Reflect.getPrototypeOf(scopes[0]);\n },\n\n /**\n * A trap for the in operator.\n */\n has(target, prop: string) {\n return getKeysFromAllScopes(target).includes(prop);\n },\n\n /**\n * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.\n */\n ownKeys(target) {\n return getKeysFromAllScopes(target);\n },\n\n /**\n * A trap for setting property values.\n */\n set(target, prop: string, value) {\n const storage = target._storage || (target._storage = getTarget());\n target[prop] = storage[prop] = value; // set to top level scope + cache\n delete target._keys; // remove cached keys\n return true;\n }\n }) as ResolverProxy;\n}\n\n/**\n * Returns an Proxy for resolving option values with context.\n * @param proxy - The Proxy returned by `_createResolver`\n * @param context - Context object for scriptable/indexable options\n * @param subProxy - The proxy provided for scriptable options\n * @param descriptorDefaults - Defaults for descriptors\n * @private\n */\nexport function _attachContext<\n T extends AnyObject[] = AnyObject[],\n R extends AnyObject[] = T\n>(\n proxy: ResolverProxy,\n context: AnyObject,\n subProxy?: ResolverProxy,\n descriptorDefaults?: DescriptorDefaults\n) {\n const cache: ContextCache = {\n _cacheable: false,\n _proxy: proxy,\n _context: context,\n _subProxy: subProxy,\n _stack: new Set(),\n _descriptors: _descriptors(proxy, descriptorDefaults),\n setContext: (ctx: AnyObject) => _attachContext(proxy, ctx, subProxy, descriptorDefaults),\n override: (scope: AnyObject) => _attachContext(proxy.override(scope), context, subProxy, descriptorDefaults)\n };\n return new Proxy(cache, {\n /**\n * A trap for the delete operator.\n */\n deleteProperty(target, prop) {\n delete target[prop]; // remove from cache\n delete proxy[prop]; // remove from proxy\n return true;\n },\n\n /**\n * A trap for getting property values.\n */\n get(target, prop: string, receiver) {\n return _cached(target, prop,\n () => _resolveWithContext(target, prop, receiver));\n },\n\n /**\n * A trap for Object.getOwnPropertyDescriptor.\n * Also used by Object.hasOwnProperty.\n */\n getOwnPropertyDescriptor(target, prop) {\n return target._descriptors.allKeys\n ? Reflect.has(proxy, prop) ? {enumerable: true, configurable: true} : undefined\n : Reflect.getOwnPropertyDescriptor(proxy, prop);\n },\n\n /**\n * A trap for Object.getPrototypeOf.\n */\n getPrototypeOf() {\n return Reflect.getPrototypeOf(proxy);\n },\n\n /**\n * A trap for the in operator.\n */\n has(target, prop) {\n return Reflect.has(proxy, prop);\n },\n\n /**\n * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.\n */\n ownKeys() {\n return Reflect.ownKeys(proxy);\n },\n\n /**\n * A trap for setting property values.\n */\n set(target, prop, value) {\n proxy[prop] = value; // set to proxy\n delete target[prop]; // remove from cache\n return true;\n }\n }) as ContextProxy;\n}\n\n/**\n * @private\n */\nexport function _descriptors(\n proxy: ResolverCache,\n defaults: DescriptorDefaults = {scriptable: true, indexable: true}\n): Descriptor {\n const {_scriptable = defaults.scriptable, _indexable = defaults.indexable, _allKeys = defaults.allKeys} = proxy;\n return {\n allKeys: _allKeys,\n scriptable: _scriptable,\n indexable: _indexable,\n isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable,\n isIndexable: isFunction(_indexable) ? _indexable : () => _indexable\n };\n}\n\nconst readKey = (prefix: string, name: string) => prefix ? prefix + _capitalize(name) : name;\nconst needsSubResolver = (prop: string, value: unknown) => isObject(value) && prop !== 'adapters' &&\n (Object.getPrototypeOf(value) === null || value.constructor === Object);\n\nfunction _cached(\n target: AnyObject,\n prop: string,\n resolve: () => unknown\n) {\n if (Object.prototype.hasOwnProperty.call(target, prop) || prop === 'constructor') {\n return target[prop];\n }\n\n const value = resolve();\n // cache the resolved value\n target[prop] = value;\n return value;\n}\n\nfunction _resolveWithContext(\n target: ContextCache,\n prop: string,\n receiver: AnyObject\n) {\n const {_proxy, _context, _subProxy, _descriptors: descriptors} = target;\n let value = _proxy[prop]; // resolve from proxy\n\n // resolve with context\n if (isFunction(value) && descriptors.isScriptable(prop)) {\n value = _resolveScriptable(prop, value, target, receiver);\n }\n if (isArray(value) && value.length) {\n value = _resolveArray(prop, value, target, descriptors.isIndexable);\n }\n if (needsSubResolver(prop, value)) {\n // if the resolved value is an object, create a sub resolver for it\n value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors);\n }\n return value;\n}\n\nfunction _resolveScriptable(\n prop: string,\n getValue: (ctx: AnyObject, sub: AnyObject) => unknown,\n target: ContextCache,\n receiver: AnyObject\n) {\n const {_proxy, _context, _subProxy, _stack} = target;\n if (_stack.has(prop)) {\n throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop);\n }\n _stack.add(prop);\n let value = getValue(_context, _subProxy || receiver);\n _stack.delete(prop);\n if (needsSubResolver(prop, value)) {\n // When scriptable option returns an object, create a resolver on that.\n value = createSubResolver(_proxy._scopes, _proxy, prop, value);\n }\n return value;\n}\n\nfunction _resolveArray(\n prop: string,\n value: unknown[],\n target: ContextCache,\n isIndexable: (key: string) => boolean\n) {\n const {_proxy, _context, _subProxy, _descriptors: descriptors} = target;\n\n if (typeof _context.index !== 'undefined' && isIndexable(prop)) {\n return value[_context.index % value.length];\n } else if (isObject(value[0])) {\n // Array of objects, return array or resolvers\n const arr = value;\n const scopes = _proxy._scopes.filter(s => s !== arr);\n value = [];\n for (const item of arr) {\n const resolver = createSubResolver(scopes, _proxy, prop, item);\n value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors));\n }\n }\n return value;\n}\n\nfunction resolveFallback(\n fallback: ResolverObjectKey | ((prop: ResolverObjectKey, value: unknown) => ResolverObjectKey),\n prop: ResolverObjectKey,\n value: unknown\n) {\n return isFunction(fallback) ? fallback(prop, value) : fallback;\n}\n\nconst getScope = (key: ResolverObjectKey, parent: AnyObject) => key === true ? parent\n : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined;\n\nfunction addScopes(\n set: Set,\n parentScopes: AnyObject[],\n key: ResolverObjectKey,\n parentFallback: ResolverObjectKey,\n value: unknown\n) {\n for (const parent of parentScopes) {\n const scope = getScope(key, parent);\n if (scope) {\n set.add(scope);\n const fallback = resolveFallback(scope._fallback, key, value);\n if (typeof fallback !== 'undefined' && fallback !== key && fallback !== parentFallback) {\n // When we reach the descriptor that defines a new _fallback, return that.\n // The fallback will resume to that new scope.\n return fallback;\n }\n } else if (scope === false && typeof parentFallback !== 'undefined' && key !== parentFallback) {\n // Fallback to `false` results to `false`, when falling back to different key.\n // For example `interaction` from `hover` or `plugins.tooltip` and `animation` from `animations`\n return null;\n }\n }\n return false;\n}\n\nfunction createSubResolver(\n parentScopes: AnyObject[],\n resolver: ResolverCache,\n prop: ResolverObjectKey,\n value: unknown\n) {\n const rootScopes = resolver._rootScopes;\n const fallback = resolveFallback(resolver._fallback, prop, value);\n const allScopes = [...parentScopes, ...rootScopes];\n const set = new Set();\n set.add(value);\n let key = addScopesFromKey(set, allScopes, prop, fallback || prop, value);\n if (key === null) {\n return false;\n }\n if (typeof fallback !== 'undefined' && fallback !== prop) {\n key = addScopesFromKey(set, allScopes, fallback, key, value);\n if (key === null) {\n return false;\n }\n }\n return _createResolver(Array.from(set), [''], rootScopes, fallback,\n () => subGetTarget(resolver, prop as string, value));\n}\n\nfunction addScopesFromKey(\n set: Set,\n allScopes: AnyObject[],\n key: ResolverObjectKey,\n fallback: ResolverObjectKey,\n item: unknown\n) {\n while (key) {\n key = addScopes(set, allScopes, key, fallback, item);\n }\n return key;\n}\n\nfunction subGetTarget(\n resolver: ResolverCache,\n prop: string,\n value: unknown\n) {\n const parent = resolver._getTarget();\n if (!(prop in parent)) {\n parent[prop] = {};\n }\n const target = parent[prop];\n if (isArray(target) && isObject(value)) {\n // For array of objects, the object is used to store updated values\n return value;\n }\n return target || {};\n}\n\nfunction _resolveWithPrefixes(\n prop: string,\n prefixes: string[],\n scopes: AnyObject[],\n proxy: ResolverProxy\n) {\n let value: unknown;\n for (const prefix of prefixes) {\n value = _resolve(readKey(prefix, prop), scopes);\n if (typeof value !== 'undefined') {\n return needsSubResolver(prop, value)\n ? createSubResolver(scopes, proxy, prop, value)\n : value;\n }\n }\n}\n\nfunction _resolve(key: string, scopes: AnyObject[]) {\n for (const scope of scopes) {\n if (!scope) {\n continue;\n }\n const value = scope[key];\n if (typeof value !== 'undefined') {\n return value;\n }\n }\n}\n\nfunction getKeysFromAllScopes(target: ResolverCache) {\n let keys = target._keys;\n if (!keys) {\n keys = target._keys = resolveKeysFromAllScopes(target._scopes);\n }\n return keys;\n}\n\nfunction resolveKeysFromAllScopes(scopes: AnyObject[]) {\n const set = new Set();\n for (const scope of scopes) {\n for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) {\n set.add(key);\n }\n }\n return Array.from(set);\n}\n\nexport function _parseObjectDataRadialScale(\n meta: ChartMeta<'line' | 'scatter'>,\n data: AnyObject[],\n start: number,\n count: number\n) {\n const {iScale} = meta;\n const {key = 'r'} = this._parsing;\n const parsed = new Array<{r: unknown}>(count);\n let i: number, ilen: number, index: number, item: AnyObject;\n\n for (i = 0, ilen = count; i < ilen; ++i) {\n index = i + start;\n item = data[index];\n parsed[i] = {\n r: iScale.parse(resolveObjectKey(item, key), index)\n };\n }\n return parsed;\n}\n","import {almostEquals, distanceBetweenPoints, sign} from './helpers.math.js';\nimport {_isPointInArea} from './helpers.canvas.js';\nimport type {ChartArea} from '../types/index.js';\nimport type {SplinePoint} from '../types/geometric.js';\n\nconst EPSILON = Number.EPSILON || 1e-14;\n\ntype OptionalSplinePoint = SplinePoint | false\nconst getPoint = (points: SplinePoint[], i: number): OptionalSplinePoint => i < points.length && !points[i].skip && points[i];\nconst getValueAxis = (indexAxis: 'x' | 'y') => indexAxis === 'x' ? 'y' : 'x';\n\nexport function splineCurve(\n firstPoint: SplinePoint,\n middlePoint: SplinePoint,\n afterPoint: SplinePoint,\n t: number\n): {\n previous: SplinePoint\n next: SplinePoint\n } {\n // Props to Rob Spencer at scaled innovation for his post on splining between points\n // http://scaledinnovation.com/analytics/splines/aboutSplines.html\n\n // This function must also respect \"skipped\" points\n\n const previous = firstPoint.skip ? middlePoint : firstPoint;\n const current = middlePoint;\n const next = afterPoint.skip ? middlePoint : afterPoint;\n const d01 = distanceBetweenPoints(current, previous);\n const d12 = distanceBetweenPoints(next, current);\n\n let s01 = d01 / (d01 + d12);\n let s12 = d12 / (d01 + d12);\n\n // If all points are the same, s01 & s02 will be inf\n s01 = isNaN(s01) ? 0 : s01;\n s12 = isNaN(s12) ? 0 : s12;\n\n const fa = t * s01; // scaling factor for triangle Ta\n const fb = t * s12;\n\n return {\n previous: {\n x: current.x - fa * (next.x - previous.x),\n y: current.y - fa * (next.y - previous.y)\n },\n next: {\n x: current.x + fb * (next.x - previous.x),\n y: current.y + fb * (next.y - previous.y)\n }\n };\n}\n\n/**\n * Adjust tangents to ensure monotonic properties\n */\nfunction monotoneAdjust(points: SplinePoint[], deltaK: number[], mK: number[]) {\n const pointsLen = points.length;\n\n let alphaK: number, betaK: number, tauK: number, squaredMagnitude: number, pointCurrent: OptionalSplinePoint;\n let pointAfter = getPoint(points, 0);\n for (let i = 0; i < pointsLen - 1; ++i) {\n pointCurrent = pointAfter;\n pointAfter = getPoint(points, i + 1);\n if (!pointCurrent || !pointAfter) {\n continue;\n }\n\n if (almostEquals(deltaK[i], 0, EPSILON)) {\n mK[i] = mK[i + 1] = 0;\n continue;\n }\n\n alphaK = mK[i] / deltaK[i];\n betaK = mK[i + 1] / deltaK[i];\n squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2);\n if (squaredMagnitude <= 9) {\n continue;\n }\n\n tauK = 3 / Math.sqrt(squaredMagnitude);\n mK[i] = alphaK * tauK * deltaK[i];\n mK[i + 1] = betaK * tauK * deltaK[i];\n }\n}\n\nfunction monotoneCompute(points: SplinePoint[], mK: number[], indexAxis: 'x' | 'y' = 'x') {\n const valueAxis = getValueAxis(indexAxis);\n const pointsLen = points.length;\n let delta: number, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint;\n let pointAfter = getPoint(points, 0);\n\n for (let i = 0; i < pointsLen; ++i) {\n pointBefore = pointCurrent;\n pointCurrent = pointAfter;\n pointAfter = getPoint(points, i + 1);\n if (!pointCurrent) {\n continue;\n }\n\n const iPixel = pointCurrent[indexAxis];\n const vPixel = pointCurrent[valueAxis];\n if (pointBefore) {\n delta = (iPixel - pointBefore[indexAxis]) / 3;\n pointCurrent[`cp1${indexAxis}`] = iPixel - delta;\n pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i];\n }\n if (pointAfter) {\n delta = (pointAfter[indexAxis] - iPixel) / 3;\n pointCurrent[`cp2${indexAxis}`] = iPixel + delta;\n pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i];\n }\n }\n}\n\n/**\n * This function calculates Bézier control points in a similar way than |splineCurve|,\n * but preserves monotonicity of the provided data and ensures no local extremums are added\n * between the dataset discrete points due to the interpolation.\n * See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation\n */\nexport function splineCurveMonotone(points: SplinePoint[], indexAxis: 'x' | 'y' = 'x') {\n const valueAxis = getValueAxis(indexAxis);\n const pointsLen = points.length;\n const deltaK: number[] = Array(pointsLen).fill(0);\n const mK: number[] = Array(pointsLen);\n\n // Calculate slopes (deltaK) and initialize tangents (mK)\n let i, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint;\n let pointAfter = getPoint(points, 0);\n\n for (i = 0; i < pointsLen; ++i) {\n pointBefore = pointCurrent;\n pointCurrent = pointAfter;\n pointAfter = getPoint(points, i + 1);\n if (!pointCurrent) {\n continue;\n }\n\n if (pointAfter) {\n const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis];\n\n // In the case of two points that appear at the same x pixel, slopeDeltaX is 0\n deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0;\n }\n mK[i] = !pointBefore ? deltaK[i]\n : !pointAfter ? deltaK[i - 1]\n : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0\n : (deltaK[i - 1] + deltaK[i]) / 2;\n }\n\n monotoneAdjust(points, deltaK, mK);\n\n monotoneCompute(points, mK, indexAxis);\n}\n\nfunction capControlPoint(pt: number, min: number, max: number) {\n return Math.max(Math.min(pt, max), min);\n}\n\nfunction capBezierPoints(points: SplinePoint[], area: ChartArea) {\n let i, ilen, point, inArea, inAreaPrev;\n let inAreaNext = _isPointInArea(points[0], area);\n for (i = 0, ilen = points.length; i < ilen; ++i) {\n inAreaPrev = inArea;\n inArea = inAreaNext;\n inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area);\n if (!inArea) {\n continue;\n }\n point = points[i];\n if (inAreaPrev) {\n point.cp1x = capControlPoint(point.cp1x, area.left, area.right);\n point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom);\n }\n if (inAreaNext) {\n point.cp2x = capControlPoint(point.cp2x, area.left, area.right);\n point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom);\n }\n }\n}\n\n/**\n * @private\n */\nexport function _updateBezierControlPoints(\n points: SplinePoint[],\n options,\n area: ChartArea,\n loop: boolean,\n indexAxis: 'x' | 'y'\n) {\n let i: number, ilen: number, point: SplinePoint, controlPoints: ReturnType;\n\n // Only consider points that are drawn in case the spanGaps option is used\n if (options.spanGaps) {\n points = points.filter((pt) => !pt.skip);\n }\n\n if (options.cubicInterpolationMode === 'monotone') {\n splineCurveMonotone(points, indexAxis);\n } else {\n let prev = loop ? points[points.length - 1] : points[0];\n for (i = 0, ilen = points.length; i < ilen; ++i) {\n point = points[i];\n controlPoints = splineCurve(\n prev,\n point,\n points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen],\n options.tension\n );\n point.cp1x = controlPoints.previous.x;\n point.cp1y = controlPoints.previous.y;\n point.cp2x = controlPoints.next.x;\n point.cp2y = controlPoints.next.y;\n prev = point;\n }\n }\n\n if (options.capBezierPoints) {\n capBezierPoints(points, area);\n }\n}\n","import {PI, TAU, HALF_PI} from './helpers.math.js';\n\nconst atEdge = (t: number) => t === 0 || t === 1;\nconst elasticIn = (t: number, s: number, p: number) => -(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p));\nconst elasticOut = (t: number, s: number, p: number) => Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1;\n\n/**\n * Easing functions adapted from Robert Penner's easing equations.\n * @namespace Chart.helpers.easing.effects\n * @see http://www.robertpenner.com/easing/\n */\nconst effects = {\n linear: (t: number) => t,\n\n easeInQuad: (t: number) => t * t,\n\n easeOutQuad: (t: number) => -t * (t - 2),\n\n easeInOutQuad: (t: number) => ((t /= 0.5) < 1)\n ? 0.5 * t * t\n : -0.5 * ((--t) * (t - 2) - 1),\n\n easeInCubic: (t: number) => t * t * t,\n\n easeOutCubic: (t: number) => (t -= 1) * t * t + 1,\n\n easeInOutCubic: (t: number) => ((t /= 0.5) < 1)\n ? 0.5 * t * t * t\n : 0.5 * ((t -= 2) * t * t + 2),\n\n easeInQuart: (t: number) => t * t * t * t,\n\n easeOutQuart: (t: number) => -((t -= 1) * t * t * t - 1),\n\n easeInOutQuart: (t: number) => ((t /= 0.5) < 1)\n ? 0.5 * t * t * t * t\n : -0.5 * ((t -= 2) * t * t * t - 2),\n\n easeInQuint: (t: number) => t * t * t * t * t,\n\n easeOutQuint: (t: number) => (t -= 1) * t * t * t * t + 1,\n\n easeInOutQuint: (t: number) => ((t /= 0.5) < 1)\n ? 0.5 * t * t * t * t * t\n : 0.5 * ((t -= 2) * t * t * t * t + 2),\n\n easeInSine: (t: number) => -Math.cos(t * HALF_PI) + 1,\n\n easeOutSine: (t: number) => Math.sin(t * HALF_PI),\n\n easeInOutSine: (t: number) => -0.5 * (Math.cos(PI * t) - 1),\n\n easeInExpo: (t: number) => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)),\n\n easeOutExpo: (t: number) => (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1,\n\n easeInOutExpo: (t: number) => atEdge(t) ? t : t < 0.5\n ? 0.5 * Math.pow(2, 10 * (t * 2 - 1))\n : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2),\n\n easeInCirc: (t: number) => (t >= 1) ? t : -(Math.sqrt(1 - t * t) - 1),\n\n easeOutCirc: (t: number) => Math.sqrt(1 - (t -= 1) * t),\n\n easeInOutCirc: (t: number) => ((t /= 0.5) < 1)\n ? -0.5 * (Math.sqrt(1 - t * t) - 1)\n : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1),\n\n easeInElastic: (t: number) => atEdge(t) ? t : elasticIn(t, 0.075, 0.3),\n\n easeOutElastic: (t: number) => atEdge(t) ? t : elasticOut(t, 0.075, 0.3),\n\n easeInOutElastic(t: number) {\n const s = 0.1125;\n const p = 0.45;\n return atEdge(t) ? t :\n t < 0.5\n ? 0.5 * elasticIn(t * 2, s, p)\n : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p);\n },\n\n easeInBack(t: number) {\n const s = 1.70158;\n return t * t * ((s + 1) * t - s);\n },\n\n easeOutBack(t: number) {\n const s = 1.70158;\n return (t -= 1) * t * ((s + 1) * t + s) + 1;\n },\n\n easeInOutBack(t: number) {\n let s = 1.70158;\n if ((t /= 0.5) < 1) {\n return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s));\n }\n return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);\n },\n\n easeInBounce: (t: number) => 1 - effects.easeOutBounce(1 - t),\n\n easeOutBounce(t: number) {\n const m = 7.5625;\n const d = 2.75;\n if (t < (1 / d)) {\n return m * t * t;\n }\n if (t < (2 / d)) {\n return m * (t -= (1.5 / d)) * t + 0.75;\n }\n if (t < (2.5 / d)) {\n return m * (t -= (2.25 / d)) * t + 0.9375;\n }\n return m * (t -= (2.625 / d)) * t + 0.984375;\n },\n\n easeInOutBounce: (t: number) => (t < 0.5)\n ? effects.easeInBounce(t * 2) * 0.5\n : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5,\n} as const;\n\nexport type EasingFunction = keyof typeof effects\n\nexport default effects;\n","import type {Point, SplinePoint} from '../types/geometric.js';\n\n/**\n * @private\n */\nexport function _pointInLine(p1: Point, p2: Point, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars\n return {\n x: p1.x + t * (p2.x - p1.x),\n y: p1.y + t * (p2.y - p1.y)\n };\n}\n\n/**\n * @private\n */\nexport function _steppedInterpolation(\n p1: Point,\n p2: Point,\n t: number, mode: 'middle' | 'after' | unknown\n) {\n return {\n x: p1.x + t * (p2.x - p1.x),\n y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y\n : mode === 'after' ? t < 1 ? p1.y : p2.y\n : t > 0 ? p2.y : p1.y\n };\n}\n\n/**\n * @private\n */\nexport function _bezierInterpolation(p1: SplinePoint, p2: SplinePoint, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars\n const cp1 = {x: p1.cp2x, y: p1.cp2y};\n const cp2 = {x: p2.cp1x, y: p2.cp1y};\n const a = _pointInLine(p1, cp1, t);\n const b = _pointInLine(cp1, cp2, t);\n const c = _pointInLine(cp2, p2, t);\n const d = _pointInLine(a, b, t);\n const e = _pointInLine(b, c, t);\n return _pointInLine(d, e, t);\n}\n","import defaults from '../core/core.defaults.js';\nimport {isArray, isObject, toDimension, valueOrDefault} from './helpers.core.js';\nimport {toFontString} from './helpers.canvas.js';\nimport type {ChartArea, FontSpec, Point} from '../types/index.js';\nimport type {TRBL, TRBLCorners} from '../types/geometric.js';\n\nconst LINE_HEIGHT = /^(normal|(\\d+(?:\\.\\d+)?)(px|em|%)?)$/;\nconst FONT_STYLE = /^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;\n\n/**\n * @alias Chart.helpers.options\n * @namespace\n */\n/**\n * Converts the given line height `value` in pixels for a specific font `size`.\n * @param value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em').\n * @param size - The font size (in pixels) used to resolve relative `value`.\n * @returns The effective line height in pixels (size * 1.2 if value is invalid).\n * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height\n * @since 2.7.0\n */\nexport function toLineHeight(value: number | string, size: number): number {\n const matches = ('' + value).match(LINE_HEIGHT);\n if (!matches || matches[1] === 'normal') {\n return size * 1.2;\n }\n\n value = +matches[2];\n\n switch (matches[3]) {\n case 'px':\n return value;\n case '%':\n value /= 100;\n break;\n default:\n break;\n }\n\n return size * value;\n}\n\nconst numberOrZero = (v: unknown) => +v || 0;\n\n/**\n * @param value\n * @param props\n */\nexport function _readValueToProps(value: number | Record, props: K[]): Record;\nexport function _readValueToProps(value: number | Record, props: Record): Record;\nexport function _readValueToProps(value: number | Record, props: string[] | Record) {\n const ret = {};\n const objProps = isObject(props);\n const keys = objProps ? Object.keys(props) : props;\n const read = isObject(value)\n ? objProps\n ? prop => valueOrDefault(value[prop], value[props[prop]])\n : prop => value[prop]\n : () => value;\n\n for (const prop of keys) {\n ret[prop] = numberOrZero(read(prop));\n }\n return ret;\n}\n\n/**\n * Converts the given value into a TRBL object.\n * @param value - If a number, set the value to all TRBL component,\n * else, if an object, use defined properties and sets undefined ones to 0.\n * x / y are shorthands for same value for left/right and top/bottom.\n * @returns The padding values (top, right, bottom, left)\n * @since 3.0.0\n */\nexport function toTRBL(value: number | TRBL | Point) {\n return _readValueToProps(value, {top: 'y', right: 'x', bottom: 'y', left: 'x'});\n}\n\n/**\n * Converts the given value into a TRBL corners object (similar with css border-radius).\n * @param value - If a number, set the value to all TRBL corner components,\n * else, if an object, use defined properties and sets undefined ones to 0.\n * @returns The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight)\n * @since 3.0.0\n */\nexport function toTRBLCorners(value: number | TRBLCorners) {\n return _readValueToProps(value, ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']);\n}\n\n/**\n * Converts the given value into a padding object with pre-computed width/height.\n * @param value - If a number, set the value to all TRBL component,\n * else, if an object, use defined properties and sets undefined ones to 0.\n * x / y are shorthands for same value for left/right and top/bottom.\n * @returns The padding values (top, right, bottom, left, width, height)\n * @since 2.7.0\n */\nexport function toPadding(value?: number | TRBL): ChartArea {\n const obj = toTRBL(value) as ChartArea;\n\n obj.width = obj.left + obj.right;\n obj.height = obj.top + obj.bottom;\n\n return obj;\n}\n\n/**\n * Parses font options and returns the font object.\n * @param options - A object that contains font options to be parsed.\n * @param fallback - A object that contains fallback font options.\n * @return The font object.\n * @private\n */\n\nexport function toFont(options: Partial, fallback?: Partial) {\n options = options || {};\n fallback = fallback || defaults.font as FontSpec;\n\n let size = valueOrDefault(options.size, fallback.size);\n\n if (typeof size === 'string') {\n size = parseInt(size, 10);\n }\n let style = valueOrDefault(options.style, fallback.style);\n if (style && !('' + style).match(FONT_STYLE)) {\n console.warn('Invalid font style specified: \"' + style + '\"');\n style = undefined;\n }\n\n const font = {\n family: valueOrDefault(options.family, fallback.family),\n lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size),\n size,\n style,\n weight: valueOrDefault(options.weight, fallback.weight),\n string: ''\n };\n\n font.string = toFontString(font);\n return font;\n}\n\n/**\n * Evaluates the given `inputs` sequentially and returns the first defined value.\n * @param inputs - An array of values, falling back to the last value.\n * @param context - If defined and the current value is a function, the value\n * is called with `context` as first argument and the result becomes the new input.\n * @param index - If defined and the current value is an array, the value\n * at `index` become the new input.\n * @param info - object to return information about resolution in\n * @param info.cacheable - Will be set to `false` if option is not cacheable.\n * @since 2.7.0\n */\nexport function resolve(inputs: Array, context?: object, index?: number, info?: { cacheable: boolean }) {\n let cacheable = true;\n let i: number, ilen: number, value: unknown;\n\n for (i = 0, ilen = inputs.length; i < ilen; ++i) {\n value = inputs[i];\n if (value === undefined) {\n continue;\n }\n if (context !== undefined && typeof value === 'function') {\n value = value(context);\n cacheable = false;\n }\n if (index !== undefined && isArray(value)) {\n value = value[index % value.length];\n cacheable = false;\n }\n if (value !== undefined) {\n if (info && !cacheable) {\n info.cacheable = false;\n }\n return value;\n }\n }\n}\n\n/**\n * @param minmax\n * @param grace\n * @param beginAtZero\n * @private\n */\nexport function _addGrace(minmax: { min: number; max: number; }, grace: number | string, beginAtZero: boolean) {\n const {min, max} = minmax;\n const change = toDimension(grace, (max - min) / 2);\n const keepZero = (value: number, add: number) => beginAtZero && value === 0 ? 0 : value + add;\n return {\n min: keepZero(min, -Math.abs(change)),\n max: keepZero(max, change)\n };\n}\n\n/**\n * Create a context inheriting parentContext\n * @param parentContext\n * @param context\n * @returns\n */\nexport function createContext(parentContext: null, context: T): T;\nexport function createContext(parentContext: P, context: T): P & T;\nexport function createContext(parentContext: object, context: object) {\n return Object.assign(Object.create(parentContext), context);\n}\n","export interface RTLAdapter {\n x(x: number): number;\n setWidth(w: number): void;\n textAlign(align: 'center' | 'left' | 'right'): 'center' | 'left' | 'right';\n xPlus(x: number, value: number): number;\n leftForLtr(x: number, itemWidth: number): number;\n}\n\nconst getRightToLeftAdapter = function(rectX: number, width: number): RTLAdapter {\n return {\n x(x) {\n return rectX + rectX + width - x;\n },\n setWidth(w) {\n width = w;\n },\n textAlign(align) {\n if (align === 'center') {\n return align;\n }\n return align === 'right' ? 'left' : 'right';\n },\n xPlus(x, value) {\n return x - value;\n },\n leftForLtr(x, itemWidth) {\n return x - itemWidth;\n },\n };\n};\n\nconst getLeftToRightAdapter = function(): RTLAdapter {\n return {\n x(x) {\n return x;\n },\n setWidth(w) { // eslint-disable-line no-unused-vars\n },\n textAlign(align) {\n return align;\n },\n xPlus(x, value) {\n return x + value;\n },\n leftForLtr(x, _itemWidth) { // eslint-disable-line @typescript-eslint/no-unused-vars\n return x;\n },\n };\n};\n\nexport function getRtlAdapter(rtl: boolean, rectX: number, width: number) {\n return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter();\n}\n\nexport function overrideTextDirection(ctx: CanvasRenderingContext2D, direction: 'ltr' | 'rtl') {\n let style: CSSStyleDeclaration, original: [string, string];\n if (direction === 'ltr' || direction === 'rtl') {\n style = ctx.canvas.style;\n original = [\n style.getPropertyValue('direction'),\n style.getPropertyPriority('direction'),\n ];\n\n style.setProperty('direction', direction, 'important');\n (ctx as { prevTextDirection?: [string, string] }).prevTextDirection = original;\n }\n}\n\nexport function restoreTextDirection(ctx: CanvasRenderingContext2D, original?: [string, string]) {\n if (original !== undefined) {\n delete (ctx as { prevTextDirection?: [string, string] }).prevTextDirection;\n ctx.canvas.style.setProperty('direction', original[0], original[1]);\n }\n}\n","import {_angleBetween, _angleDiff, _isBetween, _normalizeAngle} from './helpers.math.js';\nimport {createContext} from './helpers.options.js';\nimport {isPatternOrGradient} from './helpers.color.js';\n\n/**\n * @typedef { import('../elements/element.line.js').default } LineElement\n * @typedef { import('../elements/element.point.js').default } PointElement\n * @typedef {{start: number, end: number, loop: boolean, style?: any}} Segment\n */\n\nfunction propertyFn(property) {\n if (property === 'angle') {\n return {\n between: _angleBetween,\n compare: _angleDiff,\n normalize: _normalizeAngle,\n };\n }\n return {\n between: _isBetween,\n compare: (a, b) => a - b,\n normalize: x => x\n };\n}\n\nfunction normalizeSegment({start, end, count, loop, style}) {\n return {\n start: start % count,\n end: end % count,\n loop: loop && (end - start + 1) % count === 0,\n style\n };\n}\n\nfunction getSegment(segment, points, bounds) {\n const {property, start: startBound, end: endBound} = bounds;\n const {between, normalize} = propertyFn(property);\n const count = points.length;\n // eslint-disable-next-line prefer-const\n let {start, end, loop} = segment;\n let i, ilen;\n\n if (loop) {\n start += count;\n end += count;\n for (i = 0, ilen = count; i < ilen; ++i) {\n if (!between(normalize(points[start % count][property]), startBound, endBound)) {\n break;\n }\n start--;\n end--;\n }\n start %= count;\n end %= count;\n }\n\n if (end < start) {\n end += count;\n }\n return {start, end, loop, style: segment.style};\n}\n\n/**\n * Returns the sub-segment(s) of a line segment that fall in the given bounds\n * @param {object} segment\n * @param {number} segment.start - start index of the segment, referring the points array\n * @param {number} segment.end - end index of the segment, referring the points array\n * @param {boolean} segment.loop - indicates that the segment is a loop\n * @param {object} [segment.style] - segment style\n * @param {PointElement[]} points - the points that this segment refers to\n * @param {object} [bounds]\n * @param {string} bounds.property - the property of a `PointElement` we are bounding. `x`, `y` or `angle`.\n * @param {number} bounds.start - start value of the property\n * @param {number} bounds.end - end value of the property\n * @private\n **/\nexport function _boundSegment(segment, points, bounds) {\n if (!bounds) {\n return [segment];\n }\n\n const {property, start: startBound, end: endBound} = bounds;\n const count = points.length;\n const {compare, between, normalize} = propertyFn(property);\n const {start, end, loop, style} = getSegment(segment, points, bounds);\n\n const result = [];\n let inside = false;\n let subStart = null;\n let value, point, prevValue;\n\n const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0;\n const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value);\n const shouldStart = () => inside || startIsBefore();\n const shouldStop = () => !inside || endIsBefore();\n\n for (let i = start, prev = start; i <= end; ++i) {\n point = points[i % count];\n\n if (point.skip) {\n continue;\n }\n\n value = normalize(point[property]);\n\n if (value === prevValue) {\n continue;\n }\n\n inside = between(value, startBound, endBound);\n\n if (subStart === null && shouldStart()) {\n subStart = compare(value, startBound) === 0 ? i : prev;\n }\n\n if (subStart !== null && shouldStop()) {\n result.push(normalizeSegment({start: subStart, end: i, loop, count, style}));\n subStart = null;\n }\n prev = i;\n prevValue = value;\n }\n\n if (subStart !== null) {\n result.push(normalizeSegment({start: subStart, end, loop, count, style}));\n }\n\n return result;\n}\n\n\n/**\n * Returns the segments of the line that are inside given bounds\n * @param {LineElement} line\n * @param {object} [bounds]\n * @param {string} bounds.property - the property we are bounding with. `x`, `y` or `angle`.\n * @param {number} bounds.start - start value of the `property`\n * @param {number} bounds.end - end value of the `property`\n * @private\n */\nexport function _boundSegments(line, bounds) {\n const result = [];\n const segments = line.segments;\n\n for (let i = 0; i < segments.length; i++) {\n const sub = _boundSegment(segments[i], line.points, bounds);\n if (sub.length) {\n result.push(...sub);\n }\n }\n return result;\n}\n\n/**\n * Find start and end index of a line.\n */\nfunction findStartAndEnd(points, count, loop, spanGaps) {\n let start = 0;\n let end = count - 1;\n\n if (loop && !spanGaps) {\n // loop and not spanning gaps, first find a gap to start from\n while (start < count && !points[start].skip) {\n start++;\n }\n }\n\n // find first non skipped point (after the first gap possibly)\n while (start < count && points[start].skip) {\n start++;\n }\n\n // if we looped to count, start needs to be 0\n start %= count;\n\n if (loop) {\n // loop will go past count, if start > 0\n end += start;\n }\n\n while (end > start && points[end % count].skip) {\n end--;\n }\n\n // end could be more than count, normalize\n end %= count;\n\n return {start, end};\n}\n\n/**\n * Compute solid segments from Points, when spanGaps === false\n * @param {PointElement[]} points - the points\n * @param {number} start - start index\n * @param {number} max - max index (can go past count on a loop)\n * @param {boolean} loop - boolean indicating that this would be a loop if no gaps are found\n */\nfunction solidSegments(points, start, max, loop) {\n const count = points.length;\n const result = [];\n let last = start;\n let prev = points[start];\n let end;\n\n for (end = start + 1; end <= max; ++end) {\n const cur = points[end % count];\n if (cur.skip || cur.stop) {\n if (!prev.skip) {\n loop = false;\n result.push({start: start % count, end: (end - 1) % count, loop});\n // @ts-ignore\n start = last = cur.stop ? end : null;\n }\n } else {\n last = end;\n if (prev.skip) {\n start = end;\n }\n }\n prev = cur;\n }\n\n if (last !== null) {\n result.push({start: start % count, end: last % count, loop});\n }\n\n return result;\n}\n\n/**\n * Compute the continuous segments that define the whole line\n * There can be skipped points within a segment, if spanGaps is true.\n * @param {LineElement} line\n * @param {object} [segmentOptions]\n * @return {Segment[]}\n * @private\n */\nexport function _computeSegments(line, segmentOptions) {\n const points = line.points;\n const spanGaps = line.options.spanGaps;\n const count = points.length;\n\n if (!count) {\n return [];\n }\n\n const loop = !!line._loop;\n const {start, end} = findStartAndEnd(points, count, loop, spanGaps);\n\n if (spanGaps === true) {\n return splitByStyles(line, [{start, end, loop}], points, segmentOptions);\n }\n\n const max = end < start ? end + count : end;\n const completeLoop = !!line._fullLoop && start === 0 && end === count - 1;\n return splitByStyles(line, solidSegments(points, start, max, completeLoop), points, segmentOptions);\n}\n\n/**\n * @param {Segment[]} segments\n * @param {PointElement[]} points\n * @param {object} [segmentOptions]\n * @return {Segment[]}\n */\nfunction splitByStyles(line, segments, points, segmentOptions) {\n if (!segmentOptions || !segmentOptions.setContext || !points) {\n return segments;\n }\n return doSplitByStyles(line, segments, points, segmentOptions);\n}\n\n/**\n * @param {LineElement} line\n * @param {Segment[]} segments\n * @param {PointElement[]} points\n * @param {object} [segmentOptions]\n * @return {Segment[]}\n */\nfunction doSplitByStyles(line, segments, points, segmentOptions) {\n const chartContext = line._chart.getContext();\n const baseStyle = readStyle(line.options);\n const {_datasetIndex: datasetIndex, options: {spanGaps}} = line;\n const count = points.length;\n const result = [];\n let prevStyle = baseStyle;\n let start = segments[0].start;\n let i = start;\n\n function addStyle(s, e, l, st) {\n const dir = spanGaps ? -1 : 1;\n if (s === e) {\n return;\n }\n // Style can not start/end on a skipped point, adjust indices accordingly\n s += count;\n while (points[s % count].skip) {\n s -= dir;\n }\n while (points[e % count].skip) {\n e += dir;\n }\n if (s % count !== e % count) {\n result.push({start: s % count, end: e % count, loop: l, style: st});\n prevStyle = st;\n start = e % count;\n }\n }\n\n for (const segment of segments) {\n start = spanGaps ? start : segment.start;\n let prev = points[start % count];\n let style;\n for (i = start + 1; i <= segment.end; i++) {\n const pt = points[i % count];\n style = readStyle(segmentOptions.setContext(createContext(chartContext, {\n type: 'segment',\n p0: prev,\n p1: pt,\n p0DataIndex: (i - 1) % count,\n p1DataIndex: i % count,\n datasetIndex\n })));\n if (styleChanged(style, prevStyle)) {\n addStyle(start, i - 1, segment.loop, prevStyle);\n }\n prev = pt;\n prevStyle = style;\n }\n if (start < i - 1) {\n addStyle(start, i - 1, segment.loop, prevStyle);\n }\n }\n\n return result;\n}\n\nfunction readStyle(options) {\n return {\n backgroundColor: options.backgroundColor,\n borderCapStyle: options.borderCapStyle,\n borderDash: options.borderDash,\n borderDashOffset: options.borderDashOffset,\n borderJoinStyle: options.borderJoinStyle,\n borderWidth: options.borderWidth,\n borderColor: options.borderColor\n };\n}\n\nfunction styleChanged(style, prevStyle) {\n if (!prevStyle) {\n return false;\n }\n const cache = [];\n const replacer = function(key, value) {\n if (!isPatternOrGradient(value)) {\n return value;\n }\n if (!cache.includes(value)) {\n cache.push(value);\n }\n return cache.indexOf(value);\n };\n return JSON.stringify(style, replacer) !== JSON.stringify(prevStyle, replacer);\n}\n","import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection.js';\nimport {getRelativePosition} from '../helpers/helpers.dom.js';\nimport {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math.js';\nimport {_isPointInArea} from '../helpers/index.js';\n\n/**\n * @typedef { import('./core.controller.js').default } Chart\n * @typedef { import('../types/index.js').ChartEvent } ChartEvent\n * @typedef {{axis?: string, intersect?: boolean, includeInvisible?: boolean}} InteractionOptions\n * @typedef {{datasetIndex: number, index: number, element: import('./core.element.js').default}} InteractionItem\n * @typedef { import('../types/index.js').Point } Point\n */\n\n/**\n * Helper function to do binary search when possible\n * @param {object} metaset - the dataset meta\n * @param {string} axis - the axis mode. x|y|xy|r\n * @param {number} value - the value to find\n * @param {boolean} [intersect] - should the element intersect\n * @returns {{lo:number, hi:number}} indices to search data array between\n */\nfunction binarySearch(metaset, axis, value, intersect) {\n const {controller, data, _sorted} = metaset;\n const iScale = controller._cachedMeta.iScale;\n if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) {\n const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey;\n if (!intersect) {\n return lookupMethod(data, axis, value);\n } else if (controller._sharedOptions) {\n // _sharedOptions indicates that each element has equal options -> equal proportions\n // So we can do a ranged binary search based on the range of first element and\n // be confident to get the full range of indices that can intersect with the value.\n const el = data[0];\n const range = typeof el.getRange === 'function' && el.getRange(axis);\n if (range) {\n const start = lookupMethod(data, axis, value - range);\n const end = lookupMethod(data, axis, value + range);\n return {lo: start.lo, hi: end.hi};\n }\n }\n }\n // Default to all elements, when binary search can not be used.\n return {lo: 0, hi: data.length - 1};\n}\n\n/**\n * Helper function to select candidate elements for interaction\n * @param {Chart} chart - the chart\n * @param {string} axis - the axis mode. x|y|xy|r\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {function} handler - the callback to execute for each visible item\n * @param {boolean} [intersect] - consider intersecting items\n */\nfunction evaluateInteractionItems(chart, axis, position, handler, intersect) {\n const metasets = chart.getSortedVisibleDatasetMetas();\n const value = position[axis];\n for (let i = 0, ilen = metasets.length; i < ilen; ++i) {\n const {index, data} = metasets[i];\n const {lo, hi} = binarySearch(metasets[i], axis, value, intersect);\n for (let j = lo; j <= hi; ++j) {\n const element = data[j];\n if (!element.skip) {\n handler(element, index, j);\n }\n }\n }\n}\n\n/**\n * Get a distance metric function for two points based on the\n * axis mode setting\n * @param {string} axis - the axis mode. x|y|xy|r\n */\nfunction getDistanceMetricForAxis(axis) {\n const useX = axis.indexOf('x') !== -1;\n const useY = axis.indexOf('y') !== -1;\n\n return function(pt1, pt2) {\n const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0;\n const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0;\n return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));\n };\n}\n\n/**\n * Helper function to get the items that intersect the event position\n * @param {Chart} chart - the chart\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {string} axis - the axis mode. x|y|xy|r\n * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position\n * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area\n * @return {InteractionItem[]} the nearest items\n */\nfunction getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) {\n const items = [];\n\n if (!includeInvisible && !chart.isPointInArea(position)) {\n return items;\n }\n\n const evaluationFunc = function(element, datasetIndex, index) {\n if (!includeInvisible && !_isPointInArea(element, chart.chartArea, 0)) {\n return;\n }\n if (element.inRange(position.x, position.y, useFinalPosition)) {\n items.push({element, datasetIndex, index});\n }\n };\n\n evaluateInteractionItems(chart, axis, position, evaluationFunc, true);\n return items;\n}\n\n/**\n * Helper function to get the items nearest to the event position for a radial chart\n * @param {Chart} chart - the chart to look at elements from\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {string} axis - the axes along which to measure distance\n * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position\n * @return {InteractionItem[]} the nearest items\n */\nfunction getNearestRadialItems(chart, position, axis, useFinalPosition) {\n let items = [];\n\n function evaluationFunc(element, datasetIndex, index) {\n const {startAngle, endAngle} = element.getProps(['startAngle', 'endAngle'], useFinalPosition);\n const {angle} = getAngleFromPoint(element, {x: position.x, y: position.y});\n\n if (_angleBetween(angle, startAngle, endAngle)) {\n items.push({element, datasetIndex, index});\n }\n }\n\n evaluateInteractionItems(chart, axis, position, evaluationFunc);\n return items;\n}\n\n/**\n * Helper function to get the items nearest to the event position for a cartesian chart\n * @param {Chart} chart - the chart to look at elements from\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {string} axis - the axes along which to measure distance\n * @param {boolean} [intersect] - if true, only consider items that intersect the position\n * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position\n * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area\n * @return {InteractionItem[]} the nearest items\n */\nfunction getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {\n let items = [];\n const distanceMetric = getDistanceMetricForAxis(axis);\n let minDistance = Number.POSITIVE_INFINITY;\n\n function evaluationFunc(element, datasetIndex, index) {\n const inRange = element.inRange(position.x, position.y, useFinalPosition);\n if (intersect && !inRange) {\n return;\n }\n\n const center = element.getCenterPoint(useFinalPosition);\n const pointInArea = !!includeInvisible || chart.isPointInArea(center);\n if (!pointInArea && !inRange) {\n return;\n }\n\n const distance = distanceMetric(position, center);\n if (distance < minDistance) {\n items = [{element, datasetIndex, index}];\n minDistance = distance;\n } else if (distance === minDistance) {\n // Can have multiple items at the same distance in which case we sort by size\n items.push({element, datasetIndex, index});\n }\n }\n\n evaluateInteractionItems(chart, axis, position, evaluationFunc);\n return items;\n}\n\n/**\n * Helper function to get the items nearest to the event position considering all visible items in the chart\n * @param {Chart} chart - the chart to look at elements from\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {string} axis - the axes along which to measure distance\n * @param {boolean} [intersect] - if true, only consider items that intersect the position\n * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position\n * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area\n * @return {InteractionItem[]} the nearest items\n */\nfunction getNearestItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) {\n if (!includeInvisible && !chart.isPointInArea(position)) {\n return [];\n }\n\n return axis === 'r' && !intersect\n ? getNearestRadialItems(chart, position, axis, useFinalPosition)\n : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible);\n}\n\n/**\n * Helper function to get the items matching along the given X or Y axis\n * @param {Chart} chart - the chart to look at elements from\n * @param {Point} position - the point to be nearest to, in relative coordinates\n * @param {string} axis - the axis to match\n * @param {boolean} [intersect] - if true, only consider items that intersect the position\n * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position\n * @return {InteractionItem[]} the nearest items\n */\nfunction getAxisItems(chart, position, axis, intersect, useFinalPosition) {\n const items = [];\n const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange';\n let intersectsItem = false;\n\n evaluateInteractionItems(chart, axis, position, (element, datasetIndex, index) => {\n if (element[rangeMethod] && element[rangeMethod](position[axis], useFinalPosition)) {\n items.push({element, datasetIndex, index});\n intersectsItem = intersectsItem || element.inRange(position.x, position.y, useFinalPosition);\n }\n });\n\n // If we want to trigger on an intersect and we don't have any items\n // that intersect the position, return nothing\n if (intersect && !intersectsItem) {\n return [];\n }\n return items;\n}\n\n/**\n * Contains interaction related functions\n * @namespace Chart.Interaction\n */\nexport default {\n // Part of the public API to facilitate developers creating their own modes\n evaluateInteractionItems,\n\n // Helper function for different modes\n modes: {\n /**\n\t\t * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something\n\t\t * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item\n\t\t * @function Chart.Interaction.modes.index\n\t\t * @since v2.4.0\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n index(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n // Default axis for index mode is 'x' to match old behaviour\n const axis = options.axis || 'x';\n const includeInvisible = options.includeInvisible || false;\n const items = options.intersect\n ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible)\n : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);\n const elements = [];\n\n if (!items.length) {\n return [];\n }\n\n chart.getSortedVisibleDatasetMetas().forEach((meta) => {\n const index = items[0].index;\n const element = meta.data[index];\n\n // don't count items that are skipped (null data)\n if (element && !element.skip) {\n elements.push({element, datasetIndex: meta.index, index});\n }\n });\n\n return elements;\n },\n\n /**\n\t\t * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something\n\t\t * If the options.intersect is false, we find the nearest item and return the items in that dataset\n\t\t * @function Chart.Interaction.modes.dataset\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n dataset(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n const axis = options.axis || 'xy';\n const includeInvisible = options.includeInvisible || false;\n let items = options.intersect\n ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) :\n getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible);\n\n if (items.length > 0) {\n const datasetIndex = items[0].datasetIndex;\n const data = chart.getDatasetMeta(datasetIndex).data;\n items = [];\n for (let i = 0; i < data.length; ++i) {\n items.push({element: data[i], datasetIndex, index: i});\n }\n }\n\n return items;\n },\n\n /**\n\t\t * Point mode returns all elements that hit test based on the event position\n\t\t * of the event\n\t\t * @function Chart.Interaction.modes.intersect\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n point(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n const axis = options.axis || 'xy';\n const includeInvisible = options.includeInvisible || false;\n return getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible);\n },\n\n /**\n\t\t * nearest mode returns the element closest to the point\n\t\t * @function Chart.Interaction.modes.intersect\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n nearest(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n const axis = options.axis || 'xy';\n const includeInvisible = options.includeInvisible || false;\n return getNearestItems(chart, position, axis, options.intersect, useFinalPosition, includeInvisible);\n },\n\n /**\n\t\t * x mode returns the elements that hit-test at the current x coordinate\n\t\t * @function Chart.Interaction.modes.x\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n x(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n return getAxisItems(chart, position, 'x', options.intersect, useFinalPosition);\n },\n\n /**\n\t\t * y mode returns the elements that hit-test at the current y coordinate\n\t\t * @function Chart.Interaction.modes.y\n\t\t * @param {Chart} chart - the chart we are returning items from\n\t\t * @param {Event} e - the event we are find things at\n\t\t * @param {InteractionOptions} options - options to use\n\t\t * @param {boolean} [useFinalPosition] - use final element position (animation target)\n\t\t * @return {InteractionItem[]} - items that are found\n\t\t */\n y(chart, e, options, useFinalPosition) {\n const position = getRelativePosition(e, chart);\n return getAxisItems(chart, position, 'y', options.intersect, useFinalPosition);\n }\n }\n};\n","import {defined, each, isObject} from '../helpers/helpers.core.js';\nimport {toPadding} from '../helpers/helpers.options.js';\n\n/**\n * @typedef { import('./core.controller.js').default } Chart\n */\n\nconst STATIC_POSITIONS = ['left', 'top', 'right', 'bottom'];\n\nfunction filterByPosition(array, position) {\n return array.filter(v => v.pos === position);\n}\n\nfunction filterDynamicPositionByAxis(array, axis) {\n return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis);\n}\n\nfunction sortByWeight(array, reverse) {\n return array.sort((a, b) => {\n const v0 = reverse ? b : a;\n const v1 = reverse ? a : b;\n return v0.weight === v1.weight ?\n v0.index - v1.index :\n v0.weight - v1.weight;\n });\n}\n\nfunction wrapBoxes(boxes) {\n const layoutBoxes = [];\n let i, ilen, box, pos, stack, stackWeight;\n\n for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) {\n box = boxes[i];\n ({position: pos, options: {stack, stackWeight = 1}} = box);\n layoutBoxes.push({\n index: i,\n box,\n pos,\n horizontal: box.isHorizontal(),\n weight: box.weight,\n stack: stack && (pos + stack),\n stackWeight\n });\n }\n return layoutBoxes;\n}\n\nfunction buildStacks(layouts) {\n const stacks = {};\n for (const wrap of layouts) {\n const {stack, pos, stackWeight} = wrap;\n if (!stack || !STATIC_POSITIONS.includes(pos)) {\n continue;\n }\n const _stack = stacks[stack] || (stacks[stack] = {count: 0, placed: 0, weight: 0, size: 0});\n _stack.count++;\n _stack.weight += stackWeight;\n }\n return stacks;\n}\n\n/**\n * store dimensions used instead of available chartArea in fitBoxes\n **/\nfunction setLayoutDims(layouts, params) {\n const stacks = buildStacks(layouts);\n const {vBoxMaxWidth, hBoxMaxHeight} = params;\n let i, ilen, layout;\n for (i = 0, ilen = layouts.length; i < ilen; ++i) {\n layout = layouts[i];\n const {fullSize} = layout.box;\n const stack = stacks[layout.stack];\n const factor = stack && layout.stackWeight / stack.weight;\n if (layout.horizontal) {\n layout.width = factor ? factor * vBoxMaxWidth : fullSize && params.availableWidth;\n layout.height = hBoxMaxHeight;\n } else {\n layout.width = vBoxMaxWidth;\n layout.height = factor ? factor * hBoxMaxHeight : fullSize && params.availableHeight;\n }\n }\n return stacks;\n}\n\nfunction buildLayoutBoxes(boxes) {\n const layoutBoxes = wrapBoxes(boxes);\n const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true);\n const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true);\n const right = sortByWeight(filterByPosition(layoutBoxes, 'right'));\n const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true);\n const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom'));\n const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x');\n const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y');\n\n return {\n fullSize,\n leftAndTop: left.concat(top),\n rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal),\n chartArea: filterByPosition(layoutBoxes, 'chartArea'),\n vertical: left.concat(right).concat(centerVertical),\n horizontal: top.concat(bottom).concat(centerHorizontal)\n };\n}\n\nfunction getCombinedMax(maxPadding, chartArea, a, b) {\n return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]);\n}\n\nfunction updateMaxPadding(maxPadding, boxPadding) {\n maxPadding.top = Math.max(maxPadding.top, boxPadding.top);\n maxPadding.left = Math.max(maxPadding.left, boxPadding.left);\n maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom);\n maxPadding.right = Math.max(maxPadding.right, boxPadding.right);\n}\n\nfunction updateDims(chartArea, params, layout, stacks) {\n const {pos, box} = layout;\n const maxPadding = chartArea.maxPadding;\n\n // dynamically placed boxes size is not considered\n if (!isObject(pos)) {\n if (layout.size) {\n // this layout was already counted for, lets first reduce old size\n chartArea[pos] -= layout.size;\n }\n const stack = stacks[layout.stack] || {size: 0, count: 1};\n stack.size = Math.max(stack.size, layout.horizontal ? box.height : box.width);\n layout.size = stack.size / stack.count;\n chartArea[pos] += layout.size;\n }\n\n if (box.getPadding) {\n updateMaxPadding(maxPadding, box.getPadding());\n }\n\n const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right'));\n const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom'));\n const widthChanged = newWidth !== chartArea.w;\n const heightChanged = newHeight !== chartArea.h;\n chartArea.w = newWidth;\n chartArea.h = newHeight;\n\n // return booleans on the changes per direction\n return layout.horizontal\n ? {same: widthChanged, other: heightChanged}\n : {same: heightChanged, other: widthChanged};\n}\n\nfunction handleMaxPadding(chartArea) {\n const maxPadding = chartArea.maxPadding;\n\n function updatePos(pos) {\n const change = Math.max(maxPadding[pos] - chartArea[pos], 0);\n chartArea[pos] += change;\n return change;\n }\n chartArea.y += updatePos('top');\n chartArea.x += updatePos('left');\n updatePos('right');\n updatePos('bottom');\n}\n\nfunction getMargins(horizontal, chartArea) {\n const maxPadding = chartArea.maxPadding;\n\n function marginForPositions(positions) {\n const margin = {left: 0, top: 0, right: 0, bottom: 0};\n positions.forEach((pos) => {\n margin[pos] = Math.max(chartArea[pos], maxPadding[pos]);\n });\n return margin;\n }\n\n return horizontal\n ? marginForPositions(['left', 'right'])\n : marginForPositions(['top', 'bottom']);\n}\n\nfunction fitBoxes(boxes, chartArea, params, stacks) {\n const refitBoxes = [];\n let i, ilen, layout, box, refit, changed;\n\n for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) {\n layout = boxes[i];\n box = layout.box;\n\n box.update(\n layout.width || chartArea.w,\n layout.height || chartArea.h,\n getMargins(layout.horizontal, chartArea)\n );\n const {same, other} = updateDims(chartArea, params, layout, stacks);\n\n // Dimensions changed and there were non full width boxes before this\n // -> we have to refit those\n refit |= same && refitBoxes.length;\n\n // Chart area changed in the opposite direction\n changed = changed || other;\n\n if (!box.fullSize) { // fullSize boxes don't need to be re-fitted in any case\n refitBoxes.push(layout);\n }\n }\n\n return refit && fitBoxes(refitBoxes, chartArea, params, stacks) || changed;\n}\n\nfunction setBoxDims(box, left, top, width, height) {\n box.top = top;\n box.left = left;\n box.right = left + width;\n box.bottom = top + height;\n box.width = width;\n box.height = height;\n}\n\nfunction placeBoxes(boxes, chartArea, params, stacks) {\n const userPadding = params.padding;\n let {x, y} = chartArea;\n\n for (const layout of boxes) {\n const box = layout.box;\n const stack = stacks[layout.stack] || {count: 1, placed: 0, weight: 1};\n const weight = (layout.stackWeight / stack.weight) || 1;\n if (layout.horizontal) {\n const width = chartArea.w * weight;\n const height = stack.size || box.height;\n if (defined(stack.start)) {\n y = stack.start;\n }\n if (box.fullSize) {\n setBoxDims(box, userPadding.left, y, params.outerWidth - userPadding.right - userPadding.left, height);\n } else {\n setBoxDims(box, chartArea.left + stack.placed, y, width, height);\n }\n stack.start = y;\n stack.placed += width;\n y = box.bottom;\n } else {\n const height = chartArea.h * weight;\n const width = stack.size || box.width;\n if (defined(stack.start)) {\n x = stack.start;\n }\n if (box.fullSize) {\n setBoxDims(box, x, userPadding.top, width, params.outerHeight - userPadding.bottom - userPadding.top);\n } else {\n setBoxDims(box, x, chartArea.top + stack.placed, width, height);\n }\n stack.start = x;\n stack.placed += height;\n x = box.right;\n }\n }\n\n chartArea.x = x;\n chartArea.y = y;\n}\n\n/**\n * @interface LayoutItem\n * @typedef {object} LayoutItem\n * @prop {string} position - The position of the item in the chart layout. Possible values are\n * 'left', 'top', 'right', 'bottom', and 'chartArea'\n * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area\n * @prop {boolean} fullSize - if true, and the item is horizontal, then push vertical boxes down\n * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom)\n * @prop {function} update - Takes two parameters: width and height. Returns size of item\n * @prop {function} draw - Draws the element\n * @prop {function} [getPadding] - Returns an object with padding on the edges\n * @prop {number} width - Width of item. Must be valid after update()\n * @prop {number} height - Height of item. Must be valid after update()\n * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update\n * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update\n * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update\n * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update\n */\n\n// The layout service is very self explanatory. It's responsible for the layout within a chart.\n// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need\n// It is this service's responsibility of carrying out that layout.\nexport default {\n\n /**\n\t * Register a box to a chart.\n\t * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title.\n\t * @param {Chart} chart - the chart to use\n\t * @param {LayoutItem} item - the item to add to be laid out\n\t */\n addBox(chart, item) {\n if (!chart.boxes) {\n chart.boxes = [];\n }\n\n // initialize item with default values\n item.fullSize = item.fullSize || false;\n item.position = item.position || 'top';\n item.weight = item.weight || 0;\n // @ts-ignore\n item._layers = item._layers || function() {\n return [{\n z: 0,\n draw(chartArea) {\n item.draw(chartArea);\n }\n }];\n };\n\n chart.boxes.push(item);\n },\n\n /**\n\t * Remove a layoutItem from a chart\n\t * @param {Chart} chart - the chart to remove the box from\n\t * @param {LayoutItem} layoutItem - the item to remove from the layout\n\t */\n removeBox(chart, layoutItem) {\n const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1;\n if (index !== -1) {\n chart.boxes.splice(index, 1);\n }\n },\n\n /**\n\t * Sets (or updates) options on the given `item`.\n\t * @param {Chart} chart - the chart in which the item lives (or will be added to)\n\t * @param {LayoutItem} item - the item to configure with the given options\n\t * @param {object} options - the new item options.\n\t */\n configure(chart, item, options) {\n item.fullSize = options.fullSize;\n item.position = options.position;\n item.weight = options.weight;\n },\n\n /**\n\t * Fits boxes of the given chart into the given size by having each box measure itself\n\t * then running a fitting algorithm\n\t * @param {Chart} chart - the chart\n\t * @param {number} width - the width to fit into\n\t * @param {number} height - the height to fit into\n * @param {number} minPadding - minimum padding required for each side of chart area\n\t */\n update(chart, width, height, minPadding) {\n if (!chart) {\n return;\n }\n\n const padding = toPadding(chart.options.layout.padding);\n const availableWidth = Math.max(width - padding.width, 0);\n const availableHeight = Math.max(height - padding.height, 0);\n const boxes = buildLayoutBoxes(chart.boxes);\n const verticalBoxes = boxes.vertical;\n const horizontalBoxes = boxes.horizontal;\n\n // Before any changes are made, notify boxes that an update is about to being\n // This is used to clear any cached data (e.g. scale limits)\n each(chart.boxes, box => {\n if (typeof box.beforeLayout === 'function') {\n box.beforeLayout();\n }\n });\n\n // Essentially we now have any number of boxes on each of the 4 sides.\n // Our canvas looks like the following.\n // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and\n // B1 is the bottom axis\n // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays\n // These locations are single-box locations only, when trying to register a chartArea location that is already taken,\n // an error will be thrown.\n //\n // |----------------------------------------------------|\n // | T1 (Full Width) |\n // |----------------------------------------------------|\n // | | | T2 | |\n // | |----|-------------------------------------|----|\n // | | | C1 | | C2 | |\n // | | |----| |----| |\n // | | | | |\n // | L1 | L2 | ChartArea (C0) | R1 |\n // | | | | |\n // | | |----| |----| |\n // | | | C3 | | C4 | |\n // | |----|-------------------------------------|----|\n // | | | B1 | |\n // |----------------------------------------------------|\n // | B2 (Full Width) |\n // |----------------------------------------------------|\n //\n\n const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) =>\n wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1;\n\n const params = Object.freeze({\n outerWidth: width,\n outerHeight: height,\n padding,\n availableWidth,\n availableHeight,\n vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount,\n hBoxMaxHeight: availableHeight / 2\n });\n const maxPadding = Object.assign({}, padding);\n updateMaxPadding(maxPadding, toPadding(minPadding));\n const chartArea = Object.assign({\n maxPadding,\n w: availableWidth,\n h: availableHeight,\n x: padding.left,\n y: padding.top\n }, padding);\n\n const stacks = setLayoutDims(verticalBoxes.concat(horizontalBoxes), params);\n\n // First fit the fullSize boxes, to reduce probability of re-fitting.\n fitBoxes(boxes.fullSize, chartArea, params, stacks);\n\n // Then fit vertical boxes\n fitBoxes(verticalBoxes, chartArea, params, stacks);\n\n // Then fit horizontal boxes\n if (fitBoxes(horizontalBoxes, chartArea, params, stacks)) {\n // if the area changed, re-fit vertical boxes\n fitBoxes(verticalBoxes, chartArea, params, stacks);\n }\n\n handleMaxPadding(chartArea);\n\n // Finally place the boxes to correct coordinates\n placeBoxes(boxes.leftAndTop, chartArea, params, stacks);\n\n // Move to opposite side of chart\n chartArea.x += chartArea.w;\n chartArea.y += chartArea.h;\n\n placeBoxes(boxes.rightAndBottom, chartArea, params, stacks);\n\n chart.chartArea = {\n left: chartArea.left,\n top: chartArea.top,\n right: chartArea.left + chartArea.w,\n bottom: chartArea.top + chartArea.h,\n height: chartArea.h,\n width: chartArea.w,\n };\n\n // Finally update boxes in chartArea (radial scale for example)\n each(boxes.chartArea, (layout) => {\n const box = layout.box;\n Object.assign(box, chart.chartArea);\n box.update(chartArea.w, chartArea.h, {left: 0, top: 0, right: 0, bottom: 0});\n });\n }\n};\n","\n/**\n * @typedef { import('../core/core.controller.js').default } Chart\n */\n\n/**\n * Abstract class that allows abstracting platform dependencies away from the chart.\n */\nexport default class BasePlatform {\n /**\n\t * Called at chart construction time, returns a context2d instance implementing\n\t * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}.\n\t * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific)\n\t * @param {number} [aspectRatio] - The chart options\n\t */\n acquireContext(canvas, aspectRatio) {} // eslint-disable-line no-unused-vars\n\n /**\n\t * Called at chart destruction time, releases any resources associated to the context\n\t * previously returned by the acquireContext() method.\n\t * @param {CanvasRenderingContext2D} context - The context2d instance\n\t * @returns {boolean} true if the method succeeded, else false\n\t */\n releaseContext(context) { // eslint-disable-line no-unused-vars\n return false;\n }\n\n /**\n\t * Registers the specified listener on the given chart.\n\t * @param {Chart} chart - Chart from which to listen for event\n\t * @param {string} type - The ({@link ChartEvent}) type to listen for\n\t * @param {function} listener - Receives a notification (an object that implements\n\t * the {@link ChartEvent} interface) when an event of the specified type occurs.\n\t */\n addEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars\n\n /**\n\t * Removes the specified listener previously registered with addEventListener.\n\t * @param {Chart} chart - Chart from which to remove the listener\n\t * @param {string} type - The ({@link ChartEvent}) type to remove\n\t * @param {function} listener - The listener function to remove from the event target.\n\t */\n removeEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars\n\n /**\n\t * @returns {number} the current devicePixelRatio of the device this platform is connected to.\n\t */\n getDevicePixelRatio() {\n return 1;\n }\n\n /**\n\t * Returns the maximum size in pixels of given canvas element.\n\t * @param {HTMLCanvasElement} element\n\t * @param {number} [width] - content width of parent element\n\t * @param {number} [height] - content height of parent element\n\t * @param {number} [aspectRatio] - aspect ratio to maintain\n\t */\n getMaximumSize(element, width, height, aspectRatio) {\n width = Math.max(0, width || element.width);\n height = height || element.height;\n return {\n width,\n height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height)\n };\n }\n\n /**\n\t * @param {HTMLCanvasElement} canvas\n\t * @returns {boolean} true if the canvas is attached to the platform, false if not.\n\t */\n isAttached(canvas) { // eslint-disable-line no-unused-vars\n return true;\n }\n\n /**\n * Updates config with platform specific requirements\n * @param {import('../core/core.config.js').default} config\n */\n updateConfig(config) { // eslint-disable-line no-unused-vars\n // no-op\n }\n}\n","/**\n * Platform fallback implementation (minimal).\n * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939\n */\n\nimport BasePlatform from './platform.base.js';\n\n/**\n * Platform class for charts without access to the DOM or to many element properties\n * This platform is used by default for any chart passed an OffscreenCanvas.\n * @extends BasePlatform\n */\nexport default class BasicPlatform extends BasePlatform {\n acquireContext(item) {\n // To prevent canvas fingerprinting, some add-ons undefine the getContext\n // method, for example: https://github.com/kkapsner/CanvasBlocker\n // https://github.com/chartjs/Chart.js/issues/2807\n return item && item.getContext && item.getContext('2d') || null;\n }\n updateConfig(config) {\n config.options.animation = false;\n }\n}\n","/**\n * Chart.Platform implementation for targeting a web browser\n */\n\nimport BasePlatform from './platform.base.js';\nimport {_getParentNode, getRelativePosition, supportsEventListenerOptions, readUsedSize, getMaximumSize} from '../helpers/helpers.dom.js';\nimport {throttled} from '../helpers/helpers.extras.js';\nimport {isNullOrUndef} from '../helpers/helpers.core.js';\n\n/**\n * @typedef { import('../core/core.controller.js').default } Chart\n */\n\nconst EXPANDO_KEY = '$chartjs';\n\n/**\n * DOM event types -> Chart.js event types.\n * Note: only events with different types are mapped.\n * @see https://developer.mozilla.org/en-US/docs/Web/Events\n */\nconst EVENT_TYPES = {\n touchstart: 'mousedown',\n touchmove: 'mousemove',\n touchend: 'mouseup',\n pointerenter: 'mouseenter',\n pointerdown: 'mousedown',\n pointermove: 'mousemove',\n pointerup: 'mouseup',\n pointerleave: 'mouseout',\n pointerout: 'mouseout'\n};\n\nconst isNullOrEmpty = value => value === null || value === '';\n/**\n * Initializes the canvas style and render size without modifying the canvas display size,\n * since responsiveness is handled by the controller.resize() method. The config is used\n * to determine the aspect ratio to apply in case no explicit height has been specified.\n * @param {HTMLCanvasElement} canvas\n * @param {number} [aspectRatio]\n */\nfunction initCanvas(canvas, aspectRatio) {\n const style = canvas.style;\n\n // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it\n // returns null or '' if no explicit value has been set to the canvas attribute.\n const renderHeight = canvas.getAttribute('height');\n const renderWidth = canvas.getAttribute('width');\n\n // Chart.js modifies some canvas values that we want to restore on destroy\n canvas[EXPANDO_KEY] = {\n initial: {\n height: renderHeight,\n width: renderWidth,\n style: {\n display: style.display,\n height: style.height,\n width: style.width\n }\n }\n };\n\n // Force canvas to display as block to avoid extra space caused by inline\n // elements, which would interfere with the responsive resize process.\n // https://github.com/chartjs/Chart.js/issues/2538\n style.display = style.display || 'block';\n // Include possible borders in the size\n style.boxSizing = style.boxSizing || 'border-box';\n\n if (isNullOrEmpty(renderWidth)) {\n const displayWidth = readUsedSize(canvas, 'width');\n if (displayWidth !== undefined) {\n canvas.width = displayWidth;\n }\n }\n\n if (isNullOrEmpty(renderHeight)) {\n if (canvas.style.height === '') {\n // If no explicit render height and style height, let's apply the aspect ratio,\n // which one can be specified by the user but also by charts as default option\n // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2.\n canvas.height = canvas.width / (aspectRatio || 2);\n } else {\n const displayHeight = readUsedSize(canvas, 'height');\n if (displayHeight !== undefined) {\n canvas.height = displayHeight;\n }\n }\n }\n\n return canvas;\n}\n\n// Default passive to true as expected by Chrome for 'touchstart' and 'touchend' events.\n// https://github.com/chartjs/Chart.js/issues/4287\nconst eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false;\n\nfunction addListener(node, type, listener) {\n if (node) {\n node.addEventListener(type, listener, eventListenerOptions);\n }\n}\n\nfunction removeListener(chart, type, listener) {\n if (chart && chart.canvas) {\n chart.canvas.removeEventListener(type, listener, eventListenerOptions);\n }\n}\n\nfunction fromNativeEvent(event, chart) {\n const type = EVENT_TYPES[event.type] || event.type;\n const {x, y} = getRelativePosition(event, chart);\n return {\n type,\n chart,\n native: event,\n x: x !== undefined ? x : null,\n y: y !== undefined ? y : null,\n };\n}\n\nfunction nodeListContains(nodeList, canvas) {\n for (const node of nodeList) {\n if (node === canvas || node.contains(canvas)) {\n return true;\n }\n }\n}\n\nfunction createAttachObserver(chart, type, listener) {\n const canvas = chart.canvas;\n const observer = new MutationObserver(entries => {\n let trigger = false;\n for (const entry of entries) {\n trigger = trigger || nodeListContains(entry.addedNodes, canvas);\n trigger = trigger && !nodeListContains(entry.removedNodes, canvas);\n }\n if (trigger) {\n listener();\n }\n });\n observer.observe(document, {childList: true, subtree: true});\n return observer;\n}\n\nfunction createDetachObserver(chart, type, listener) {\n const canvas = chart.canvas;\n const observer = new MutationObserver(entries => {\n let trigger = false;\n for (const entry of entries) {\n trigger = trigger || nodeListContains(entry.removedNodes, canvas);\n trigger = trigger && !nodeListContains(entry.addedNodes, canvas);\n }\n if (trigger) {\n listener();\n }\n });\n observer.observe(document, {childList: true, subtree: true});\n return observer;\n}\n\nconst drpListeningCharts = new Map();\nlet oldDevicePixelRatio = 0;\n\nfunction onWindowResize() {\n const dpr = window.devicePixelRatio;\n if (dpr === oldDevicePixelRatio) {\n return;\n }\n oldDevicePixelRatio = dpr;\n drpListeningCharts.forEach((resize, chart) => {\n if (chart.currentDevicePixelRatio !== dpr) {\n resize();\n }\n });\n}\n\nfunction listenDevicePixelRatioChanges(chart, resize) {\n if (!drpListeningCharts.size) {\n window.addEventListener('resize', onWindowResize);\n }\n drpListeningCharts.set(chart, resize);\n}\n\nfunction unlistenDevicePixelRatioChanges(chart) {\n drpListeningCharts.delete(chart);\n if (!drpListeningCharts.size) {\n window.removeEventListener('resize', onWindowResize);\n }\n}\n\nfunction createResizeObserver(chart, type, listener) {\n const canvas = chart.canvas;\n const container = canvas && _getParentNode(canvas);\n if (!container) {\n return;\n }\n const resize = throttled((width, height) => {\n const w = container.clientWidth;\n listener(width, height);\n if (w < container.clientWidth) {\n // If the container size shrank during chart resize, let's assume\n // scrollbar appeared. So we resize again with the scrollbar visible -\n // effectively making chart smaller and the scrollbar hidden again.\n // Because we are inside `throttled`, and currently `ticking`, scroll\n // events are ignored during this whole 2 resize process.\n // If we assumed wrong and something else happened, we are resizing\n // twice in a frame (potential performance issue)\n listener();\n }\n }, window);\n\n // @ts-ignore until https://github.com/microsoft/TypeScript/issues/37861 implemented\n const observer = new ResizeObserver(entries => {\n const entry = entries[0];\n const width = entry.contentRect.width;\n const height = entry.contentRect.height;\n // When its container's display is set to 'none' the callback will be called with a\n // size of (0, 0), which will cause the chart to lose its original height, so skip\n // resizing in such case.\n if (width === 0 && height === 0) {\n return;\n }\n resize(width, height);\n });\n observer.observe(container);\n listenDevicePixelRatioChanges(chart, resize);\n\n return observer;\n}\n\nfunction releaseObserver(chart, type, observer) {\n if (observer) {\n observer.disconnect();\n }\n if (type === 'resize') {\n unlistenDevicePixelRatioChanges(chart);\n }\n}\n\nfunction createProxyAndListen(chart, type, listener) {\n const canvas = chart.canvas;\n const proxy = throttled((event) => {\n // This case can occur if the chart is destroyed while waiting\n // for the throttled function to occur. We prevent crashes by checking\n // for a destroyed chart\n if (chart.ctx !== null) {\n listener(fromNativeEvent(event, chart));\n }\n }, chart);\n\n addListener(canvas, type, proxy);\n\n return proxy;\n}\n\n/**\n * Platform class for charts that can access the DOM and global window/document properties\n * @extends BasePlatform\n */\nexport default class DomPlatform extends BasePlatform {\n\n /**\n\t * @param {HTMLCanvasElement} canvas\n\t * @param {number} [aspectRatio]\n\t * @return {CanvasRenderingContext2D|null}\n\t */\n acquireContext(canvas, aspectRatio) {\n // To prevent canvas fingerprinting, some add-ons undefine the getContext\n // method, for example: https://github.com/kkapsner/CanvasBlocker\n // https://github.com/chartjs/Chart.js/issues/2807\n const context = canvas && canvas.getContext && canvas.getContext('2d');\n\n // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the canvas is\n // inside an iframe or when running in a protected environment. We could guess the\n // types from their toString() value but let's keep things flexible and assume it's\n // a sufficient condition if the canvas has a context2D which has canvas as `canvas`.\n // https://github.com/chartjs/Chart.js/issues/3887\n // https://github.com/chartjs/Chart.js/issues/4102\n // https://github.com/chartjs/Chart.js/issues/4152\n if (context && context.canvas === canvas) {\n // Load platform resources on first chart creation, to make it possible to\n // import the library before setting platform options.\n initCanvas(canvas, aspectRatio);\n return context;\n }\n\n return null;\n }\n\n /**\n\t * @param {CanvasRenderingContext2D} context\n\t */\n releaseContext(context) {\n const canvas = context.canvas;\n if (!canvas[EXPANDO_KEY]) {\n return false;\n }\n\n const initial = canvas[EXPANDO_KEY].initial;\n ['height', 'width'].forEach((prop) => {\n const value = initial[prop];\n if (isNullOrUndef(value)) {\n canvas.removeAttribute(prop);\n } else {\n canvas.setAttribute(prop, value);\n }\n });\n\n const style = initial.style || {};\n Object.keys(style).forEach((key) => {\n canvas.style[key] = style[key];\n });\n\n // The canvas render size might have been changed (and thus the state stack discarded),\n // we can't use save() and restore() to restore the initial state. So make sure that at\n // least the canvas context is reset to the default state by setting the canvas width.\n // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html\n // eslint-disable-next-line no-self-assign\n canvas.width = canvas.width;\n\n delete canvas[EXPANDO_KEY];\n return true;\n }\n\n /**\n\t *\n\t * @param {Chart} chart\n\t * @param {string} type\n\t * @param {function} listener\n\t */\n addEventListener(chart, type, listener) {\n // Can have only one listener per type, so make sure previous is removed\n this.removeEventListener(chart, type);\n\n const proxies = chart.$proxies || (chart.$proxies = {});\n const handlers = {\n attach: createAttachObserver,\n detach: createDetachObserver,\n resize: createResizeObserver\n };\n const handler = handlers[type] || createProxyAndListen;\n proxies[type] = handler(chart, type, listener);\n }\n\n\n /**\n\t * @param {Chart} chart\n\t * @param {string} type\n\t */\n removeEventListener(chart, type) {\n const proxies = chart.$proxies || (chart.$proxies = {});\n const proxy = proxies[type];\n\n if (!proxy) {\n return;\n }\n\n const handlers = {\n attach: releaseObserver,\n detach: releaseObserver,\n resize: releaseObserver\n };\n const handler = handlers[type] || removeListener;\n handler(chart, type, proxy);\n proxies[type] = undefined;\n }\n\n getDevicePixelRatio() {\n return window.devicePixelRatio;\n }\n\n /**\n\t * @param {HTMLCanvasElement} canvas\n\t * @param {number} [width] - content width of parent element\n\t * @param {number} [height] - content height of parent element\n\t * @param {number} [aspectRatio] - aspect ratio to maintain\n\t */\n getMaximumSize(canvas, width, height, aspectRatio) {\n return getMaximumSize(canvas, width, height, aspectRatio);\n }\n\n /**\n\t * @param {HTMLCanvasElement} canvas\n\t */\n isAttached(canvas) {\n const container = canvas && _getParentNode(canvas);\n return !!(container && container.isConnected);\n }\n}\n","import {_isDomSupported} from '../helpers/index.js';\nimport BasePlatform from './platform.base.js';\nimport BasicPlatform from './platform.basic.js';\nimport DomPlatform from './platform.dom.js';\n\nexport function _detectPlatform(canvas) {\n if (!_isDomSupported() || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)) {\n return BasicPlatform;\n }\n return DomPlatform;\n}\n\nexport {BasePlatform, BasicPlatform, DomPlatform};\n","import effects from '../helpers/helpers.easing.js';\nimport {resolve} from '../helpers/helpers.options.js';\nimport {color as helpersColor} from '../helpers/helpers.color.js';\n\nconst transparent = 'transparent';\nconst interpolators = {\n boolean(from, to, factor) {\n return factor > 0.5 ? to : from;\n },\n /**\n * @param {string} from\n * @param {string} to\n * @param {number} factor\n */\n color(from, to, factor) {\n const c0 = helpersColor(from || transparent);\n const c1 = c0.valid && helpersColor(to || transparent);\n return c1 && c1.valid\n ? c1.mix(c0, factor).hexString()\n : to;\n },\n number(from, to, factor) {\n return from + (to - from) * factor;\n }\n};\n\nexport default class Animation {\n constructor(cfg, target, prop, to) {\n const currentValue = target[prop];\n\n to = resolve([cfg.to, to, currentValue, cfg.from]);\n const from = resolve([cfg.from, currentValue, to]);\n\n this._active = true;\n this._fn = cfg.fn || interpolators[cfg.type || typeof from];\n this._easing = effects[cfg.easing] || effects.linear;\n this._start = Math.floor(Date.now() + (cfg.delay || 0));\n this._duration = this._total = Math.floor(cfg.duration);\n this._loop = !!cfg.loop;\n this._target = target;\n this._prop = prop;\n this._from = from;\n this._to = to;\n this._promises = undefined;\n }\n\n active() {\n return this._active;\n }\n\n update(cfg, to, date) {\n if (this._active) {\n this._notify(false);\n\n const currentValue = this._target[this._prop];\n const elapsed = date - this._start;\n const remain = this._duration - elapsed;\n this._start = date;\n this._duration = Math.floor(Math.max(remain, cfg.duration));\n this._total += elapsed;\n this._loop = !!cfg.loop;\n this._to = resolve([cfg.to, to, currentValue, cfg.from]);\n this._from = resolve([cfg.from, currentValue, to]);\n }\n }\n\n cancel() {\n if (this._active) {\n // update current evaluated value, for smoother animations\n this.tick(Date.now());\n this._active = false;\n this._notify(false);\n }\n }\n\n tick(date) {\n const elapsed = date - this._start;\n const duration = this._duration;\n const prop = this._prop;\n const from = this._from;\n const loop = this._loop;\n const to = this._to;\n let factor;\n\n this._active = from !== to && (loop || (elapsed < duration));\n\n if (!this._active) {\n this._target[prop] = to;\n this._notify(true);\n return;\n }\n\n if (elapsed < 0) {\n this._target[prop] = from;\n return;\n }\n\n factor = (elapsed / duration) % 2;\n factor = loop && factor > 1 ? 2 - factor : factor;\n factor = this._easing(Math.min(1, Math.max(0, factor)));\n\n this._target[prop] = this._fn(from, to, factor);\n }\n\n wait() {\n const promises = this._promises || (this._promises = []);\n return new Promise((res, rej) => {\n promises.push({res, rej});\n });\n }\n\n _notify(resolved) {\n const method = resolved ? 'res' : 'rej';\n const promises = this._promises || [];\n for (let i = 0; i < promises.length; i++) {\n promises[i][method]();\n }\n }\n}\n","import animator from './core.animator.js';\nimport Animation from './core.animation.js';\nimport defaults from './core.defaults.js';\nimport {isArray, isObject} from '../helpers/helpers.core.js';\n\nexport default class Animations {\n constructor(chart, config) {\n this._chart = chart;\n this._properties = new Map();\n this.configure(config);\n }\n\n configure(config) {\n if (!isObject(config)) {\n return;\n }\n\n const animationOptions = Object.keys(defaults.animation);\n const animatedProps = this._properties;\n\n Object.getOwnPropertyNames(config).forEach(key => {\n const cfg = config[key];\n if (!isObject(cfg)) {\n return;\n }\n const resolved = {};\n for (const option of animationOptions) {\n resolved[option] = cfg[option];\n }\n\n (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => {\n if (prop === key || !animatedProps.has(prop)) {\n animatedProps.set(prop, resolved);\n }\n });\n });\n }\n\n /**\n\t * Utility to handle animation of `options`.\n\t * @private\n\t */\n _animateOptions(target, values) {\n const newOptions = values.options;\n const options = resolveTargetOptions(target, newOptions);\n if (!options) {\n return [];\n }\n\n const animations = this._createAnimations(options, newOptions);\n if (newOptions.$shared) {\n // Going to shared options:\n // After all animations are done, assign the shared options object to the element\n // So any new updates to the shared options are observed\n awaitAll(target.options.$animations, newOptions).then(() => {\n target.options = newOptions;\n }, () => {\n // rejected, noop\n });\n }\n\n return animations;\n }\n\n /**\n\t * @private\n\t */\n _createAnimations(target, values) {\n const animatedProps = this._properties;\n const animations = [];\n const running = target.$animations || (target.$animations = {});\n const props = Object.keys(values);\n const date = Date.now();\n let i;\n\n for (i = props.length - 1; i >= 0; --i) {\n const prop = props[i];\n if (prop.charAt(0) === '$') {\n continue;\n }\n\n if (prop === 'options') {\n animations.push(...this._animateOptions(target, values));\n continue;\n }\n const value = values[prop];\n let animation = running[prop];\n const cfg = animatedProps.get(prop);\n\n if (animation) {\n if (cfg && animation.active()) {\n // There is an existing active animation, let's update that\n animation.update(cfg, value, date);\n continue;\n } else {\n animation.cancel();\n }\n }\n if (!cfg || !cfg.duration) {\n // not animated, set directly to new value\n target[prop] = value;\n continue;\n }\n\n running[prop] = animation = new Animation(cfg, target, prop, value);\n animations.push(animation);\n }\n return animations;\n }\n\n\n /**\n\t * Update `target` properties to new values, using configured animations\n\t * @param {object} target - object to update\n\t * @param {object} values - new target properties\n\t * @returns {boolean|undefined} - `true` if animations were started\n\t **/\n update(target, values) {\n if (this._properties.size === 0) {\n // Nothing is animated, just apply the new values.\n Object.assign(target, values);\n return;\n }\n\n const animations = this._createAnimations(target, values);\n\n if (animations.length) {\n animator.add(this._chart, animations);\n return true;\n }\n }\n}\n\nfunction awaitAll(animations, properties) {\n const running = [];\n const keys = Object.keys(properties);\n for (let i = 0; i < keys.length; i++) {\n const anim = animations[keys[i]];\n if (anim && anim.active()) {\n running.push(anim.wait());\n }\n }\n // @ts-ignore\n return Promise.all(running);\n}\n\nfunction resolveTargetOptions(target, newOptions) {\n if (!newOptions) {\n return;\n }\n let options = target.options;\n if (!options) {\n target.options = newOptions;\n return;\n }\n if (options.$shared) {\n // Going from shared options to distinct one:\n // Create new options object containing the old shared values and start updating that.\n target.options = options = Object.assign({}, options, {$shared: false, $animations: {}});\n }\n return options;\n}\n","import Animations from './core.animations.js';\nimport defaults from './core.defaults.js';\nimport {isArray, isFinite, isObject, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core.js';\nimport {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection.js';\nimport {createContext, sign} from '../helpers/index.js';\n\n/**\n * @typedef { import('./core.controller.js').default } Chart\n * @typedef { import('./core.scale.js').default } Scale\n */\n\nfunction scaleClip(scale, allowedOverflow) {\n const opts = scale && scale.options || {};\n const reverse = opts.reverse;\n const min = opts.min === undefined ? allowedOverflow : 0;\n const max = opts.max === undefined ? allowedOverflow : 0;\n return {\n start: reverse ? max : min,\n end: reverse ? min : max\n };\n}\n\nfunction defaultClip(xScale, yScale, allowedOverflow) {\n if (allowedOverflow === false) {\n return false;\n }\n const x = scaleClip(xScale, allowedOverflow);\n const y = scaleClip(yScale, allowedOverflow);\n\n return {\n top: y.end,\n right: x.end,\n bottom: y.start,\n left: x.start\n };\n}\n\nfunction toClip(value) {\n let t, r, b, l;\n\n if (isObject(value)) {\n t = value.top;\n r = value.right;\n b = value.bottom;\n l = value.left;\n } else {\n t = r = b = l = value;\n }\n\n return {\n top: t,\n right: r,\n bottom: b,\n left: l,\n disabled: value === false\n };\n}\n\nfunction getSortedDatasetIndices(chart, filterVisible) {\n const keys = [];\n const metasets = chart._getSortedDatasetMetas(filterVisible);\n let i, ilen;\n\n for (i = 0, ilen = metasets.length; i < ilen; ++i) {\n keys.push(metasets[i].index);\n }\n return keys;\n}\n\nfunction applyStack(stack, value, dsIndex, options = {}) {\n const keys = stack.keys;\n const singleMode = options.mode === 'single';\n let i, ilen, datasetIndex, otherValue;\n\n if (value === null) {\n return;\n }\n\n let found = false;\n for (i = 0, ilen = keys.length; i < ilen; ++i) {\n datasetIndex = +keys[i];\n if (datasetIndex === dsIndex) {\n found = true;\n if (options.all) {\n continue;\n }\n break;\n }\n otherValue = stack.values[datasetIndex];\n if (isFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) {\n value += otherValue;\n }\n }\n\n if (!found && !options.all) {\n return 0;\n }\n\n return value;\n}\n\nfunction convertObjectDataToArray(data, meta) {\n const {iScale, vScale} = meta;\n const iAxisKey = iScale.axis === 'x' ? 'x' : 'y';\n const vAxisKey = vScale.axis === 'x' ? 'x' : 'y';\n const keys = Object.keys(data);\n const adata = new Array(keys.length);\n let i, ilen, key;\n for (i = 0, ilen = keys.length; i < ilen; ++i) {\n key = keys[i];\n adata[i] = {\n [iAxisKey]: key,\n [vAxisKey]: data[key]\n };\n }\n return adata;\n}\n\nfunction isStacked(scale, meta) {\n const stacked = scale && scale.options.stacked;\n return stacked || (stacked === undefined && meta.stack !== undefined);\n}\n\nfunction getStackKey(indexScale, valueScale, meta) {\n return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`;\n}\n\nfunction getUserBounds(scale) {\n const {min, max, minDefined, maxDefined} = scale.getUserBounds();\n return {\n min: minDefined ? min : Number.NEGATIVE_INFINITY,\n max: maxDefined ? max : Number.POSITIVE_INFINITY\n };\n}\n\nfunction getOrCreateStack(stacks, stackKey, indexValue) {\n const subStack = stacks[stackKey] || (stacks[stackKey] = {});\n return subStack[indexValue] || (subStack[indexValue] = {});\n}\n\nfunction getLastIndexInStack(stack, vScale, positive, type) {\n for (const meta of vScale.getMatchingVisibleMetas(type).reverse()) {\n const value = stack[meta.index];\n if ((positive && value > 0) || (!positive && value < 0)) {\n return meta.index;\n }\n }\n\n return null;\n}\n\nfunction updateStacks(controller, parsed) {\n const {chart, _cachedMeta: meta} = controller;\n const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}}\n const {iScale, vScale, index: datasetIndex} = meta;\n const iAxis = iScale.axis;\n const vAxis = vScale.axis;\n const key = getStackKey(iScale, vScale, meta);\n const ilen = parsed.length;\n let stack;\n\n for (let i = 0; i < ilen; ++i) {\n const item = parsed[i];\n const {[iAxis]: index, [vAxis]: value} = item;\n const itemStacks = item._stacks || (item._stacks = {});\n stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index);\n stack[datasetIndex] = value;\n\n stack._top = getLastIndexInStack(stack, vScale, true, meta.type);\n stack._bottom = getLastIndexInStack(stack, vScale, false, meta.type);\n\n const visualValues = stack._visualValues || (stack._visualValues = {});\n visualValues[datasetIndex] = value;\n }\n}\n\nfunction getFirstScaleId(chart, axis) {\n const scales = chart.scales;\n return Object.keys(scales).filter(key => scales[key].axis === axis).shift();\n}\n\nfunction createDatasetContext(parent, index) {\n return createContext(parent,\n {\n active: false,\n dataset: undefined,\n datasetIndex: index,\n index,\n mode: 'default',\n type: 'dataset'\n }\n );\n}\n\nfunction createDataContext(parent, index, element) {\n return createContext(parent, {\n active: false,\n dataIndex: index,\n parsed: undefined,\n raw: undefined,\n element,\n index,\n mode: 'default',\n type: 'data'\n });\n}\n\nfunction clearStacks(meta, items) {\n // Not using meta.index here, because it might be already updated if the dataset changed location\n const datasetIndex = meta.controller.index;\n const axis = meta.vScale && meta.vScale.axis;\n if (!axis) {\n return;\n }\n\n items = items || meta._parsed;\n for (const parsed of items) {\n const stacks = parsed._stacks;\n if (!stacks || stacks[axis] === undefined || stacks[axis][datasetIndex] === undefined) {\n return;\n }\n delete stacks[axis][datasetIndex];\n if (stacks[axis]._visualValues !== undefined && stacks[axis]._visualValues[datasetIndex] !== undefined) {\n delete stacks[axis]._visualValues[datasetIndex];\n }\n }\n}\n\nconst isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none';\nconst cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached);\nconst createStack = (canStack, meta, chart) => canStack && !meta.hidden && meta._stacked\n && {keys: getSortedDatasetIndices(chart, true), values: null};\n\nexport default class DatasetController {\n\n /**\n * @type {any}\n */\n static defaults = {};\n\n /**\n * Element type used to generate a meta dataset (e.g. Chart.element.LineElement).\n */\n static datasetElementType = null;\n\n /**\n * Element type used to generate a meta data (e.g. Chart.element.PointElement).\n */\n static dataElementType = null;\n\n /**\n\t * @param {Chart} chart\n\t * @param {number} datasetIndex\n\t */\n constructor(chart, datasetIndex) {\n this.chart = chart;\n this._ctx = chart.ctx;\n this.index = datasetIndex;\n this._cachedDataOpts = {};\n this._cachedMeta = this.getMeta();\n this._type = this._cachedMeta.type;\n this.options = undefined;\n /** @type {boolean | object} */\n this._parsing = false;\n this._data = undefined;\n this._objectData = undefined;\n this._sharedOptions = undefined;\n this._drawStart = undefined;\n this._drawCount = undefined;\n this.enableOptionSharing = false;\n this.supportsDecimation = false;\n this.$context = undefined;\n this._syncList = [];\n this.datasetElementType = new.target.datasetElementType;\n this.dataElementType = new.target.dataElementType;\n\n this.initialize();\n }\n\n initialize() {\n const meta = this._cachedMeta;\n this.configure();\n this.linkScales();\n meta._stacked = isStacked(meta.vScale, meta);\n this.addElements();\n\n if (this.options.fill && !this.chart.isPluginEnabled('filler')) {\n console.warn(\"Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options\");\n }\n }\n\n updateIndex(datasetIndex) {\n if (this.index !== datasetIndex) {\n clearStacks(this._cachedMeta);\n }\n this.index = datasetIndex;\n }\n\n linkScales() {\n const chart = this.chart;\n const meta = this._cachedMeta;\n const dataset = this.getDataset();\n\n const chooseId = (axis, x, y, r) => axis === 'x' ? x : axis === 'r' ? r : y;\n\n const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x'));\n const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y'));\n const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r'));\n const indexAxis = meta.indexAxis;\n const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid);\n const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid);\n meta.xScale = this.getScaleForId(xid);\n meta.yScale = this.getScaleForId(yid);\n meta.rScale = this.getScaleForId(rid);\n meta.iScale = this.getScaleForId(iid);\n meta.vScale = this.getScaleForId(vid);\n }\n\n getDataset() {\n return this.chart.data.datasets[this.index];\n }\n\n getMeta() {\n return this.chart.getDatasetMeta(this.index);\n }\n\n /**\n\t * @param {string} scaleID\n\t * @return {Scale}\n\t */\n getScaleForId(scaleID) {\n return this.chart.scales[scaleID];\n }\n\n /**\n\t * @private\n\t */\n _getOtherScale(scale) {\n const meta = this._cachedMeta;\n return scale === meta.iScale\n ? meta.vScale\n : meta.iScale;\n }\n\n reset() {\n this._update('reset');\n }\n\n /**\n\t * @private\n\t */\n _destroy() {\n const meta = this._cachedMeta;\n if (this._data) {\n unlistenArrayEvents(this._data, this);\n }\n if (meta._stacked) {\n clearStacks(meta);\n }\n }\n\n /**\n\t * @private\n\t */\n _dataCheck() {\n const dataset = this.getDataset();\n const data = dataset.data || (dataset.data = []);\n const _data = this._data;\n\n // In order to correctly handle data addition/deletion animation (and thus simulate\n // real-time charts), we need to monitor these data modifications and synchronize\n // the internal metadata accordingly.\n\n if (isObject(data)) {\n const meta = this._cachedMeta;\n this._data = convertObjectDataToArray(data, meta);\n } else if (_data !== data) {\n if (_data) {\n // This case happens when the user replaced the data array instance.\n unlistenArrayEvents(_data, this);\n // Discard old parsed data and stacks\n const meta = this._cachedMeta;\n clearStacks(meta);\n meta._parsed = [];\n }\n if (data && Object.isExtensible(data)) {\n listenArrayEvents(data, this);\n }\n this._syncList = [];\n this._data = data;\n }\n }\n\n addElements() {\n const meta = this._cachedMeta;\n\n this._dataCheck();\n\n if (this.datasetElementType) {\n meta.dataset = new this.datasetElementType();\n }\n }\n\n buildOrUpdateElements(resetNewElements) {\n const meta = this._cachedMeta;\n const dataset = this.getDataset();\n let stackChanged = false;\n\n this._dataCheck();\n\n // make sure cached _stacked status is current\n const oldStacked = meta._stacked;\n meta._stacked = isStacked(meta.vScale, meta);\n\n // detect change in stack option\n if (meta.stack !== dataset.stack) {\n stackChanged = true;\n // remove values from old stack\n clearStacks(meta);\n meta.stack = dataset.stack;\n }\n\n // Re-sync meta data in case the user replaced the data array or if we missed\n // any updates and so make sure that we handle number of datapoints changing.\n this._resyncElements(resetNewElements);\n\n // if stack changed, update stack values for the whole dataset\n if (stackChanged || oldStacked !== meta._stacked) {\n updateStacks(this, meta._parsed);\n meta._stacked = isStacked(meta.vScale, meta);\n }\n }\n\n /**\n\t * Merges user-supplied and default dataset-level options\n\t * @private\n\t */\n configure() {\n const config = this.chart.config;\n const scopeKeys = config.datasetScopeKeys(this._type);\n const scopes = config.getOptionScopes(this.getDataset(), scopeKeys, true);\n this.options = config.createResolver(scopes, this.getContext());\n this._parsing = this.options.parsing;\n this._cachedDataOpts = {};\n }\n\n /**\n\t * @param {number} start\n\t * @param {number} count\n\t */\n parse(start, count) {\n const {_cachedMeta: meta, _data: data} = this;\n const {iScale, _stacked} = meta;\n const iAxis = iScale.axis;\n\n let sorted = start === 0 && count === data.length ? true : meta._sorted;\n let prev = start > 0 && meta._parsed[start - 1];\n let i, cur, parsed;\n\n if (this._parsing === false) {\n meta._parsed = data;\n meta._sorted = true;\n parsed = data;\n } else {\n if (isArray(data[start])) {\n parsed = this.parseArrayData(meta, data, start, count);\n } else if (isObject(data[start])) {\n parsed = this.parseObjectData(meta, data, start, count);\n } else {\n parsed = this.parsePrimitiveData(meta, data, start, count);\n }\n\n const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]);\n for (i = 0; i < count; ++i) {\n meta._parsed[i + start] = cur = parsed[i];\n if (sorted) {\n if (isNotInOrderComparedToPrev()) {\n sorted = false;\n }\n prev = cur;\n }\n }\n meta._sorted = sorted;\n }\n\n if (_stacked) {\n updateStacks(this, parsed);\n }\n }\n\n /**\n\t * Parse array of primitive values\n\t * @param {object} meta - dataset meta\n\t * @param {array} data - data array. Example [1,3,4]\n\t * @param {number} start - start index\n\t * @param {number} count - number of items to parse\n\t * @returns {object} parsed item - item containing index and a parsed value\n\t * for each scale id.\n\t * Example: {xScale0: 0, yScale0: 1}\n\t * @protected\n\t */\n parsePrimitiveData(meta, data, start, count) {\n const {iScale, vScale} = meta;\n const iAxis = iScale.axis;\n const vAxis = vScale.axis;\n const labels = iScale.getLabels();\n const singleScale = iScale === vScale;\n const parsed = new Array(count);\n let i, ilen, index;\n\n for (i = 0, ilen = count; i < ilen; ++i) {\n index = i + start;\n parsed[i] = {\n [iAxis]: singleScale || iScale.parse(labels[index], index),\n [vAxis]: vScale.parse(data[index], index)\n };\n }\n return parsed;\n }\n\n /**\n\t * Parse array of arrays\n\t * @param {object} meta - dataset meta\n\t * @param {array} data - data array. Example [[1,2],[3,4]]\n\t * @param {number} start - start index\n\t * @param {number} count - number of items to parse\n\t * @returns {object} parsed item - item containing index and a parsed value\n\t * for each scale id.\n\t * Example: {x: 0, y: 1}\n\t * @protected\n\t */\n parseArrayData(meta, data, start, count) {\n const {xScale, yScale} = meta;\n const parsed = new Array(count);\n let i, ilen, index, item;\n\n for (i = 0, ilen = count; i < ilen; ++i) {\n index = i + start;\n item = data[index];\n parsed[i] = {\n x: xScale.parse(item[0], index),\n y: yScale.parse(item[1], index)\n };\n }\n return parsed;\n }\n\n /**\n\t * Parse array of objects\n\t * @param {object} meta - dataset meta\n\t * @param {array} data - data array. Example [{x:1, y:5}, {x:2, y:10}]\n\t * @param {number} start - start index\n\t * @param {number} count - number of items to parse\n\t * @returns {object} parsed item - item containing index and a parsed value\n\t * for each scale id. _custom is optional\n\t * Example: {xScale0: 0, yScale0: 1, _custom: {r: 10, foo: 'bar'}}\n\t * @protected\n\t */\n parseObjectData(meta, data, start, count) {\n const {xScale, yScale} = meta;\n const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing;\n const parsed = new Array(count);\n let i, ilen, index, item;\n\n for (i = 0, ilen = count; i < ilen; ++i) {\n index = i + start;\n item = data[index];\n parsed[i] = {\n x: xScale.parse(resolveObjectKey(item, xAxisKey), index),\n y: yScale.parse(resolveObjectKey(item, yAxisKey), index)\n };\n }\n return parsed;\n }\n\n /**\n\t * @protected\n\t */\n getParsed(index) {\n return this._cachedMeta._parsed[index];\n }\n\n /**\n\t * @protected\n\t */\n getDataElement(index) {\n return this._cachedMeta.data[index];\n }\n\n /**\n\t * @protected\n\t */\n applyStack(scale, parsed, mode) {\n const chart = this.chart;\n const meta = this._cachedMeta;\n const value = parsed[scale.axis];\n const stack = {\n keys: getSortedDatasetIndices(chart, true),\n values: parsed._stacks[scale.axis]._visualValues\n };\n return applyStack(stack, value, meta.index, {mode});\n }\n\n /**\n\t * @protected\n\t */\n updateRangeFromParsed(range, scale, parsed, stack) {\n const parsedValue = parsed[scale.axis];\n let value = parsedValue === null ? NaN : parsedValue;\n const values = stack && parsed._stacks[scale.axis];\n if (stack && values) {\n stack.values = values;\n value = applyStack(stack, parsedValue, this._cachedMeta.index);\n }\n range.min = Math.min(range.min, value);\n range.max = Math.max(range.max, value);\n }\n\n /**\n\t * @protected\n\t */\n getMinMax(scale, canStack) {\n const meta = this._cachedMeta;\n const _parsed = meta._parsed;\n const sorted = meta._sorted && scale === meta.iScale;\n const ilen = _parsed.length;\n const otherScale = this._getOtherScale(scale);\n const stack = createStack(canStack, meta, this.chart);\n const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY};\n const {min: otherMin, max: otherMax} = getUserBounds(otherScale);\n let i, parsed;\n\n function _skip() {\n parsed = _parsed[i];\n const otherValue = parsed[otherScale.axis];\n return !isFinite(parsed[scale.axis]) || otherMin > otherValue || otherMax < otherValue;\n }\n\n for (i = 0; i < ilen; ++i) {\n if (_skip()) {\n continue;\n }\n this.updateRangeFromParsed(range, scale, parsed, stack);\n if (sorted) {\n // if the data is sorted, we don't need to check further from this end of array\n break;\n }\n }\n if (sorted) {\n // in the sorted case, find first non-skipped value from other end of array\n for (i = ilen - 1; i >= 0; --i) {\n if (_skip()) {\n continue;\n }\n this.updateRangeFromParsed(range, scale, parsed, stack);\n break;\n }\n }\n return range;\n }\n\n getAllParsedValues(scale) {\n const parsed = this._cachedMeta._parsed;\n const values = [];\n let i, ilen, value;\n\n for (i = 0, ilen = parsed.length; i < ilen; ++i) {\n value = parsed[i][scale.axis];\n if (isFinite(value)) {\n values.push(value);\n }\n }\n return values;\n }\n\n /**\n\t * @return {number|boolean}\n\t * @protected\n\t */\n getMaxOverflow() {\n return false;\n }\n\n /**\n\t * @protected\n\t */\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const iScale = meta.iScale;\n const vScale = meta.vScale;\n const parsed = this.getParsed(index);\n return {\n label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '',\n value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : ''\n };\n }\n\n /**\n\t * @private\n\t */\n _update(mode) {\n const meta = this._cachedMeta;\n this.update(mode || 'default');\n meta._clip = toClip(valueOrDefault(this.options.clip, defaultClip(meta.xScale, meta.yScale, this.getMaxOverflow())));\n }\n\n /**\n\t * @param {string} mode\n\t */\n update(mode) {} // eslint-disable-line no-unused-vars\n\n draw() {\n const ctx = this._ctx;\n const chart = this.chart;\n const meta = this._cachedMeta;\n const elements = meta.data || [];\n const area = chart.chartArea;\n const active = [];\n const start = this._drawStart || 0;\n const count = this._drawCount || (elements.length - start);\n const drawActiveElementsOnTop = this.options.drawActiveElementsOnTop;\n let i;\n\n if (meta.dataset) {\n meta.dataset.draw(ctx, area, start, count);\n }\n\n for (i = start; i < start + count; ++i) {\n const element = elements[i];\n if (element.hidden) {\n continue;\n }\n if (element.active && drawActiveElementsOnTop) {\n active.push(element);\n } else {\n element.draw(ctx, area);\n }\n }\n\n for (i = 0; i < active.length; ++i) {\n active[i].draw(ctx, area);\n }\n }\n\n /**\n\t * Returns a set of predefined style properties that should be used to represent the dataset\n\t * or the data if the index is specified\n\t * @param {number} index - data index\n\t * @param {boolean} [active] - true if hover\n\t * @return {object} style object\n\t */\n getStyle(index, active) {\n const mode = active ? 'active' : 'default';\n return index === undefined && this._cachedMeta.dataset\n ? this.resolveDatasetElementOptions(mode)\n : this.resolveDataElementOptions(index || 0, mode);\n }\n\n /**\n\t * @protected\n\t */\n getContext(index, active, mode) {\n const dataset = this.getDataset();\n let context;\n if (index >= 0 && index < this._cachedMeta.data.length) {\n const element = this._cachedMeta.data[index];\n context = element.$context ||\n (element.$context = createDataContext(this.getContext(), index, element));\n context.parsed = this.getParsed(index);\n context.raw = dataset.data[index];\n context.index = context.dataIndex = index;\n } else {\n context = this.$context ||\n (this.$context = createDatasetContext(this.chart.getContext(), this.index));\n context.dataset = dataset;\n context.index = context.datasetIndex = this.index;\n }\n\n context.active = !!active;\n context.mode = mode;\n return context;\n }\n\n /**\n\t * @param {string} [mode]\n\t * @protected\n\t */\n resolveDatasetElementOptions(mode) {\n return this._resolveElementOptions(this.datasetElementType.id, mode);\n }\n\n /**\n\t * @param {number} index\n\t * @param {string} [mode]\n\t * @protected\n\t */\n resolveDataElementOptions(index, mode) {\n return this._resolveElementOptions(this.dataElementType.id, mode, index);\n }\n\n /**\n\t * @private\n\t */\n _resolveElementOptions(elementType, mode = 'default', index) {\n const active = mode === 'active';\n const cache = this._cachedDataOpts;\n const cacheKey = elementType + '-' + mode;\n const cached = cache[cacheKey];\n const sharing = this.enableOptionSharing && defined(index);\n if (cached) {\n return cloneIfNotShared(cached, sharing);\n }\n const config = this.chart.config;\n const scopeKeys = config.datasetElementScopeKeys(this._type, elementType);\n const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, ''];\n const scopes = config.getOptionScopes(this.getDataset(), scopeKeys);\n const names = Object.keys(defaults.elements[elementType]);\n // context is provided as a function, and is called only if needed,\n // so we don't create a context for each element if not needed.\n const context = () => this.getContext(index, active, mode);\n const values = config.resolveNamedOptions(scopes, names, context, prefixes);\n\n if (values.$shared) {\n // `$shared` indicates this set of options can be shared between multiple elements.\n // Sharing is used to reduce number of properties to change during animation.\n values.$shared = sharing;\n\n // We cache options by `mode`, which can be 'active' for example. This enables us\n // to have the 'active' element options and 'default' options to switch between\n // when interacting.\n cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing));\n }\n\n return values;\n }\n\n\n /**\n\t * @private\n\t */\n _resolveAnimations(index, transition, active) {\n const chart = this.chart;\n const cache = this._cachedDataOpts;\n const cacheKey = `animation-${transition}`;\n const cached = cache[cacheKey];\n if (cached) {\n return cached;\n }\n let options;\n if (chart.options.animation !== false) {\n const config = this.chart.config;\n const scopeKeys = config.datasetAnimationScopeKeys(this._type, transition);\n const scopes = config.getOptionScopes(this.getDataset(), scopeKeys);\n options = config.createResolver(scopes, this.getContext(index, active, transition));\n }\n const animations = new Animations(chart, options && options.animations);\n if (options && options._cacheable) {\n cache[cacheKey] = Object.freeze(animations);\n }\n return animations;\n }\n\n /**\n\t * Utility for getting the options object shared between elements\n\t * @protected\n\t */\n getSharedOptions(options) {\n if (!options.$shared) {\n return;\n }\n return this._sharedOptions || (this._sharedOptions = Object.assign({}, options));\n }\n\n /**\n\t * Utility for determining if `options` should be included in the updated properties\n\t * @protected\n\t */\n includeOptions(mode, sharedOptions) {\n return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled;\n }\n\n /**\n * @todo v4, rename to getSharedOptions and remove excess functions\n */\n _getSharedOptions(start, mode) {\n const firstOpts = this.resolveDataElementOptions(start, mode);\n const previouslySharedOptions = this._sharedOptions;\n const sharedOptions = this.getSharedOptions(firstOpts);\n const includeOptions = this.includeOptions(mode, sharedOptions) || (sharedOptions !== previouslySharedOptions);\n this.updateSharedOptions(sharedOptions, mode, firstOpts);\n return {sharedOptions, includeOptions};\n }\n\n /**\n\t * Utility for updating an element with new properties, using animations when appropriate.\n\t * @protected\n\t */\n updateElement(element, index, properties, mode) {\n if (isDirectUpdateMode(mode)) {\n Object.assign(element, properties);\n } else {\n this._resolveAnimations(index, mode).update(element, properties);\n }\n }\n\n /**\n\t * Utility to animate the shared options, that are potentially affecting multiple elements.\n\t * @protected\n\t */\n updateSharedOptions(sharedOptions, mode, newOptions) {\n if (sharedOptions && !isDirectUpdateMode(mode)) {\n this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions);\n }\n }\n\n /**\n\t * @private\n\t */\n _setStyle(element, index, mode, active) {\n element.active = active;\n const options = this.getStyle(index, active);\n this._resolveAnimations(index, mode, active).update(element, {\n // When going from active to inactive, we need to update to the shared options.\n // This way the once hovered element will end up with the same original shared options instance, after animation.\n options: (!active && this.getSharedOptions(options)) || options\n });\n }\n\n removeHoverStyle(element, datasetIndex, index) {\n this._setStyle(element, index, 'active', false);\n }\n\n setHoverStyle(element, datasetIndex, index) {\n this._setStyle(element, index, 'active', true);\n }\n\n /**\n\t * @private\n\t */\n _removeDatasetHoverStyle() {\n const element = this._cachedMeta.dataset;\n\n if (element) {\n this._setStyle(element, undefined, 'active', false);\n }\n }\n\n /**\n\t * @private\n\t */\n _setDatasetHoverStyle() {\n const element = this._cachedMeta.dataset;\n\n if (element) {\n this._setStyle(element, undefined, 'active', true);\n }\n }\n\n /**\n\t * @private\n\t */\n _resyncElements(resetNewElements) {\n const data = this._data;\n const elements = this._cachedMeta.data;\n\n // Apply changes detected through array listeners\n for (const [method, arg1, arg2] of this._syncList) {\n this[method](arg1, arg2);\n }\n this._syncList = [];\n\n const numMeta = elements.length;\n const numData = data.length;\n const count = Math.min(numData, numMeta);\n\n if (count) {\n // TODO: It is not optimal to always parse the old data\n // This is done because we are not detecting direct assignments:\n // chart.data.datasets[0].data[5] = 10;\n // chart.data.datasets[0].data[5].y = 10;\n this.parse(0, count);\n }\n\n if (numData > numMeta) {\n this._insertElements(numMeta, numData - numMeta, resetNewElements);\n } else if (numData < numMeta) {\n this._removeElements(numData, numMeta - numData);\n }\n }\n\n /**\n\t * @private\n\t */\n _insertElements(start, count, resetNewElements = true) {\n const meta = this._cachedMeta;\n const data = meta.data;\n const end = start + count;\n let i;\n\n const move = (arr) => {\n arr.length += count;\n for (i = arr.length - 1; i >= end; i--) {\n arr[i] = arr[i - count];\n }\n };\n move(data);\n\n for (i = start; i < end; ++i) {\n data[i] = new this.dataElementType();\n }\n\n if (this._parsing) {\n move(meta._parsed);\n }\n this.parse(start, count);\n\n if (resetNewElements) {\n this.updateElements(data, start, count, 'reset');\n }\n }\n\n updateElements(element, start, count, mode) {} // eslint-disable-line no-unused-vars\n\n /**\n\t * @private\n\t */\n _removeElements(start, count) {\n const meta = this._cachedMeta;\n if (this._parsing) {\n const removed = meta._parsed.splice(start, count);\n if (meta._stacked) {\n clearStacks(meta, removed);\n }\n }\n meta.data.splice(start, count);\n }\n\n /**\n\t * @private\n */\n _sync(args) {\n if (this._parsing) {\n this._syncList.push(args);\n } else {\n const [method, arg1, arg2] = args;\n this[method](arg1, arg2);\n }\n this.chart._dataChanges.push([this.index, ...args]);\n }\n\n _onDataPush() {\n const count = arguments.length;\n this._sync(['_insertElements', this.getDataset().data.length - count, count]);\n }\n\n _onDataPop() {\n this._sync(['_removeElements', this._cachedMeta.data.length - 1, 1]);\n }\n\n _onDataShift() {\n this._sync(['_removeElements', 0, 1]);\n }\n\n _onDataSplice(start, count) {\n if (count) {\n this._sync(['_removeElements', start, count]);\n }\n const newCount = arguments.length - 2;\n if (newCount) {\n this._sync(['_insertElements', start, newCount]);\n }\n }\n\n _onDataUnshift() {\n this._sync(['_insertElements', 0, arguments.length]);\n }\n}\n","import type {AnyObject} from '../types/basic.js';\nimport type {Point} from '../types/geometric.js';\nimport type {Animation} from '../types/animation.js';\nimport {isNumber} from '../helpers/helpers.math.js';\n\nexport default class Element {\n\n static defaults = {};\n static defaultRoutes = undefined;\n\n x: number;\n y: number;\n active = false;\n options: O;\n $animations: Record;\n\n tooltipPosition(useFinalPosition: boolean): Point {\n const {x, y} = this.getProps(['x', 'y'], useFinalPosition);\n return {x, y} as Point;\n }\n\n hasValue() {\n return isNumber(this.x) && isNumber(this.y);\n }\n\n /**\n * Gets the current or final value of each prop. Can return extra properties (whole object).\n * @param props - properties to get\n * @param [final] - get the final value (animation target)\n */\n getProps

(props: P, final?: boolean): Pick;\n getProps

(props: P[], final?: boolean): Partial>;\n getProps(props: string[], final?: boolean): Partial> {\n const anims = this.$animations;\n if (!final || !anims) {\n // let's not create an object, if not needed\n return this as Record;\n }\n const ret: Record = {};\n props.forEach((prop) => {\n ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : this[prop as string];\n });\n return ret;\n }\n}\n","import {isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js';\nimport {_factorize} from '../helpers/helpers.math.js';\n\n\n/**\n * @typedef { import('./core.controller.js').default } Chart\n * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick\n */\n\n/**\n * Returns a subset of ticks to be plotted to avoid overlapping labels.\n * @param {import('./core.scale.js').default} scale\n * @param {Tick[]} ticks\n * @return {Tick[]}\n * @private\n */\nexport function autoSkip(scale, ticks) {\n const tickOpts = scale.options.ticks;\n const determinedMaxTicks = determineMaxTicks(scale);\n const ticksLimit = Math.min(tickOpts.maxTicksLimit || determinedMaxTicks, determinedMaxTicks);\n const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : [];\n const numMajorIndices = majorIndices.length;\n const first = majorIndices[0];\n const last = majorIndices[numMajorIndices - 1];\n const newTicks = [];\n\n // If there are too many major ticks to display them all\n if (numMajorIndices > ticksLimit) {\n skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit);\n return newTicks;\n }\n\n const spacing = calculateSpacing(majorIndices, ticks, ticksLimit);\n\n if (numMajorIndices > 0) {\n let i, ilen;\n const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null;\n skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first);\n for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) {\n skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]);\n }\n skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing);\n return newTicks;\n }\n skip(ticks, newTicks, spacing);\n return newTicks;\n}\n\nfunction determineMaxTicks(scale) {\n const offset = scale.options.offset;\n const tickLength = scale._tickSize();\n const maxScale = scale._length / tickLength + (offset ? 0 : 1);\n const maxChart = scale._maxLength / tickLength;\n return Math.floor(Math.min(maxScale, maxChart));\n}\n\n/**\n * @param {number[]} majorIndices\n * @param {Tick[]} ticks\n * @param {number} ticksLimit\n */\nfunction calculateSpacing(majorIndices, ticks, ticksLimit) {\n const evenMajorSpacing = getEvenSpacing(majorIndices);\n const spacing = ticks.length / ticksLimit;\n\n // If the major ticks are evenly spaced apart, place the minor ticks\n // so that they divide the major ticks into even chunks\n if (!evenMajorSpacing) {\n return Math.max(spacing, 1);\n }\n\n const factors = _factorize(evenMajorSpacing);\n for (let i = 0, ilen = factors.length - 1; i < ilen; i++) {\n const factor = factors[i];\n if (factor > spacing) {\n return factor;\n }\n }\n return Math.max(spacing, 1);\n}\n\n/**\n * @param {Tick[]} ticks\n */\nfunction getMajorIndices(ticks) {\n const result = [];\n let i, ilen;\n for (i = 0, ilen = ticks.length; i < ilen; i++) {\n if (ticks[i].major) {\n result.push(i);\n }\n }\n return result;\n}\n\n/**\n * @param {Tick[]} ticks\n * @param {Tick[]} newTicks\n * @param {number[]} majorIndices\n * @param {number} spacing\n */\nfunction skipMajors(ticks, newTicks, majorIndices, spacing) {\n let count = 0;\n let next = majorIndices[0];\n let i;\n\n spacing = Math.ceil(spacing);\n for (i = 0; i < ticks.length; i++) {\n if (i === next) {\n newTicks.push(ticks[i]);\n count++;\n next = majorIndices[count * spacing];\n }\n }\n}\n\n/**\n * @param {Tick[]} ticks\n * @param {Tick[]} newTicks\n * @param {number} spacing\n * @param {number} [majorStart]\n * @param {number} [majorEnd]\n */\nfunction skip(ticks, newTicks, spacing, majorStart, majorEnd) {\n const start = valueOrDefault(majorStart, 0);\n const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length);\n let count = 0;\n let length, i, next;\n\n spacing = Math.ceil(spacing);\n if (majorEnd) {\n length = majorEnd - majorStart;\n spacing = length / Math.floor(length / spacing);\n }\n\n next = start;\n\n while (next < 0) {\n count++;\n next = Math.round(start + count * spacing);\n }\n\n for (i = Math.max(start, 0); i < end; i++) {\n if (i === next) {\n newTicks.push(ticks[i]);\n count++;\n next = Math.round(start + count * spacing);\n }\n }\n}\n\n\n/**\n * @param {number[]} arr\n */\nfunction getEvenSpacing(arr) {\n const len = arr.length;\n let i, diff;\n\n if (len < 2) {\n return false;\n }\n\n for (diff = arr[0], i = 1; i < len; ++i) {\n if (arr[i] - arr[i - 1] !== diff) {\n return false;\n }\n }\n return diff;\n}\n","import Element from './core.element.js';\nimport {_alignPixel, _measureText, renderText, clipArea, unclipArea} from '../helpers/helpers.canvas.js';\nimport {callback as call, each, finiteOrDefault, isArray, isFinite, isNullOrUndef, isObject, valueOrDefault} from '../helpers/helpers.core.js';\nimport {toDegrees, toRadians, _int16Range, _limitValue, HALF_PI} from '../helpers/helpers.math.js';\nimport {_alignStartEnd, _toLeftRightCenter} from '../helpers/helpers.extras.js';\nimport {createContext, toFont, toPadding, _addGrace} from '../helpers/helpers.options.js';\nimport {autoSkip} from './core.scale.autoskip.js';\n\nconst reverseAlign = (align) => align === 'left' ? 'right' : align === 'right' ? 'left' : align;\nconst offsetFromEdge = (scale, edge, offset) => edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset;\nconst getTicksLimit = (ticksLength, maxTicksLimit) => Math.min(maxTicksLimit || ticksLength, ticksLength);\n\n/**\n * @typedef { import('../types/index.js').Chart } Chart\n * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick\n */\n\n/**\n * Returns a new array containing numItems from arr\n * @param {any[]} arr\n * @param {number} numItems\n */\nfunction sample(arr, numItems) {\n const result = [];\n const increment = arr.length / numItems;\n const len = arr.length;\n let i = 0;\n\n for (; i < len; i += increment) {\n result.push(arr[Math.floor(i)]);\n }\n return result;\n}\n\n/**\n * @param {Scale} scale\n * @param {number} index\n * @param {boolean} offsetGridLines\n */\nfunction getPixelForGridLine(scale, index, offsetGridLines) {\n const length = scale.ticks.length;\n const validIndex = Math.min(index, length - 1);\n const start = scale._startPixel;\n const end = scale._endPixel;\n const epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error.\n let lineValue = scale.getPixelForTick(validIndex);\n let offset;\n\n if (offsetGridLines) {\n if (length === 1) {\n offset = Math.max(lineValue - start, end - lineValue);\n } else if (index === 0) {\n offset = (scale.getPixelForTick(1) - lineValue) / 2;\n } else {\n offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2;\n }\n lineValue += validIndex < index ? offset : -offset;\n\n // Return undefined if the pixel is out of the range\n if (lineValue < start - epsilon || lineValue > end + epsilon) {\n return;\n }\n }\n return lineValue;\n}\n\n/**\n * @param {object} caches\n * @param {number} length\n */\nfunction garbageCollect(caches, length) {\n each(caches, (cache) => {\n const gc = cache.gc;\n const gcLen = gc.length / 2;\n let i;\n if (gcLen > length) {\n for (i = 0; i < gcLen; ++i) {\n delete cache.data[gc[i]];\n }\n gc.splice(0, gcLen);\n }\n });\n}\n\n/**\n * @param {object} options\n */\nfunction getTickMarkLength(options) {\n return options.drawTicks ? options.tickLength : 0;\n}\n\n/**\n * @param {object} options\n */\nfunction getTitleHeight(options, fallback) {\n if (!options.display) {\n return 0;\n }\n\n const font = toFont(options.font, fallback);\n const padding = toPadding(options.padding);\n const lines = isArray(options.text) ? options.text.length : 1;\n\n return (lines * font.lineHeight) + padding.height;\n}\n\nfunction createScaleContext(parent, scale) {\n return createContext(parent, {\n scale,\n type: 'scale'\n });\n}\n\nfunction createTickContext(parent, index, tick) {\n return createContext(parent, {\n tick,\n index,\n type: 'tick'\n });\n}\n\nfunction titleAlign(align, position, reverse) {\n /** @type {CanvasTextAlign} */\n let ret = _toLeftRightCenter(align);\n if ((reverse && position !== 'right') || (!reverse && position === 'right')) {\n ret = reverseAlign(ret);\n }\n return ret;\n}\n\nfunction titleArgs(scale, offset, position, align) {\n const {top, left, bottom, right, chart} = scale;\n const {chartArea, scales} = chart;\n let rotation = 0;\n let maxWidth, titleX, titleY;\n const height = bottom - top;\n const width = right - left;\n\n if (scale.isHorizontal()) {\n titleX = _alignStartEnd(align, left, right);\n\n if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n titleY = scales[positionAxisID].getPixelForValue(value) + height - offset;\n } else if (position === 'center') {\n titleY = (chartArea.bottom + chartArea.top) / 2 + height - offset;\n } else {\n titleY = offsetFromEdge(scale, position, offset);\n }\n maxWidth = right - left;\n } else {\n if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n titleX = scales[positionAxisID].getPixelForValue(value) - width + offset;\n } else if (position === 'center') {\n titleX = (chartArea.left + chartArea.right) / 2 - width + offset;\n } else {\n titleX = offsetFromEdge(scale, position, offset);\n }\n titleY = _alignStartEnd(align, bottom, top);\n rotation = position === 'left' ? -HALF_PI : HALF_PI;\n }\n return {titleX, titleY, maxWidth, rotation};\n}\n\nexport default class Scale extends Element {\n\n // eslint-disable-next-line max-statements\n constructor(cfg) {\n super();\n\n /** @type {string} */\n this.id = cfg.id;\n /** @type {string} */\n this.type = cfg.type;\n /** @type {any} */\n this.options = undefined;\n /** @type {CanvasRenderingContext2D} */\n this.ctx = cfg.ctx;\n /** @type {Chart} */\n this.chart = cfg.chart;\n\n // implements box\n /** @type {number} */\n this.top = undefined;\n /** @type {number} */\n this.bottom = undefined;\n /** @type {number} */\n this.left = undefined;\n /** @type {number} */\n this.right = undefined;\n /** @type {number} */\n this.width = undefined;\n /** @type {number} */\n this.height = undefined;\n this._margins = {\n left: 0,\n right: 0,\n top: 0,\n bottom: 0\n };\n /** @type {number} */\n this.maxWidth = undefined;\n /** @type {number} */\n this.maxHeight = undefined;\n /** @type {number} */\n this.paddingTop = undefined;\n /** @type {number} */\n this.paddingBottom = undefined;\n /** @type {number} */\n this.paddingLeft = undefined;\n /** @type {number} */\n this.paddingRight = undefined;\n\n // scale-specific properties\n /** @type {string=} */\n this.axis = undefined;\n /** @type {number=} */\n this.labelRotation = undefined;\n this.min = undefined;\n this.max = undefined;\n this._range = undefined;\n /** @type {Tick[]} */\n this.ticks = [];\n /** @type {object[]|null} */\n this._gridLineItems = null;\n /** @type {object[]|null} */\n this._labelItems = null;\n /** @type {object|null} */\n this._labelSizes = null;\n this._length = 0;\n this._maxLength = 0;\n this._longestTextCache = {};\n /** @type {number} */\n this._startPixel = undefined;\n /** @type {number} */\n this._endPixel = undefined;\n this._reversePixels = false;\n this._userMax = undefined;\n this._userMin = undefined;\n this._suggestedMax = undefined;\n this._suggestedMin = undefined;\n this._ticksLength = 0;\n this._borderValue = 0;\n this._cache = {};\n this._dataLimitsCached = false;\n this.$context = undefined;\n }\n\n /**\n\t * @param {any} options\n\t * @since 3.0\n\t */\n init(options) {\n this.options = options.setContext(this.getContext());\n\n this.axis = options.axis;\n\n // parse min/max value, so we can properly determine min/max for other scales\n this._userMin = this.parse(options.min);\n this._userMax = this.parse(options.max);\n this._suggestedMin = this.parse(options.suggestedMin);\n this._suggestedMax = this.parse(options.suggestedMax);\n }\n\n /**\n\t * Parse a supported input value to internal representation.\n\t * @param {*} raw\n\t * @param {number} [index]\n\t * @since 3.0\n\t */\n parse(raw, index) { // eslint-disable-line no-unused-vars\n return raw;\n }\n\n /**\n\t * @return {{min: number, max: number, minDefined: boolean, maxDefined: boolean}}\n\t * @protected\n\t * @since 3.0\n\t */\n getUserBounds() {\n let {_userMin, _userMax, _suggestedMin, _suggestedMax} = this;\n _userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY);\n _userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY);\n _suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY);\n _suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY);\n return {\n min: finiteOrDefault(_userMin, _suggestedMin),\n max: finiteOrDefault(_userMax, _suggestedMax),\n minDefined: isFinite(_userMin),\n maxDefined: isFinite(_userMax)\n };\n }\n\n /**\n\t * @param {boolean} canStack\n\t * @return {{min: number, max: number}}\n\t * @protected\n\t * @since 3.0\n\t */\n getMinMax(canStack) {\n let {min, max, minDefined, maxDefined} = this.getUserBounds();\n let range;\n\n if (minDefined && maxDefined) {\n return {min, max};\n }\n\n const metas = this.getMatchingVisibleMetas();\n for (let i = 0, ilen = metas.length; i < ilen; ++i) {\n range = metas[i].controller.getMinMax(this, canStack);\n if (!minDefined) {\n min = Math.min(min, range.min);\n }\n if (!maxDefined) {\n max = Math.max(max, range.max);\n }\n }\n\n // Make sure min <= max when only min or max is defined by user and the data is outside that range\n min = maxDefined && min > max ? max : min;\n max = minDefined && min > max ? min : max;\n\n return {\n min: finiteOrDefault(min, finiteOrDefault(max, min)),\n max: finiteOrDefault(max, finiteOrDefault(min, max))\n };\n }\n\n /**\n\t * Get the padding needed for the scale\n\t * @return {{top: number, left: number, bottom: number, right: number}} the necessary padding\n\t * @private\n\t */\n getPadding() {\n return {\n left: this.paddingLeft || 0,\n top: this.paddingTop || 0,\n right: this.paddingRight || 0,\n bottom: this.paddingBottom || 0\n };\n }\n\n /**\n\t * Returns the scale tick objects\n\t * @return {Tick[]}\n\t * @since 2.7\n\t */\n getTicks() {\n return this.ticks;\n }\n\n /**\n\t * @return {string[]}\n\t */\n getLabels() {\n const data = this.chart.data;\n return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || [];\n }\n\n /**\n * @return {import('../types.js').LabelItem[]}\n */\n getLabelItems(chartArea = this.chart.chartArea) {\n const items = this._labelItems || (this._labelItems = this._computeLabelItems(chartArea));\n return items;\n }\n\n // When a new layout is created, reset the data limits cache\n beforeLayout() {\n this._cache = {};\n this._dataLimitsCached = false;\n }\n\n // These methods are ordered by lifecycle. Utilities then follow.\n // Any function defined here is inherited by all scale types.\n // Any function can be extended by the scale type\n\n beforeUpdate() {\n call(this.options.beforeUpdate, [this]);\n }\n\n /**\n\t * @param {number} maxWidth - the max width in pixels\n\t * @param {number} maxHeight - the max height in pixels\n\t * @param {{top: number, left: number, bottom: number, right: number}} margins - the space between the edge of the other scales and edge of the chart\n\t * This space comes from two sources:\n\t * - padding - space that's required to show the labels at the edges of the scale\n\t * - thickness of scales or legends in another orientation\n\t */\n update(maxWidth, maxHeight, margins) {\n const {beginAtZero, grace, ticks: tickOpts} = this.options;\n const sampleSize = tickOpts.sampleSize;\n\n // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)\n this.beforeUpdate();\n\n // Absorb the master measurements\n this.maxWidth = maxWidth;\n this.maxHeight = maxHeight;\n this._margins = margins = Object.assign({\n left: 0,\n right: 0,\n top: 0,\n bottom: 0\n }, margins);\n\n this.ticks = null;\n this._labelSizes = null;\n this._gridLineItems = null;\n this._labelItems = null;\n\n // Dimensions\n this.beforeSetDimensions();\n this.setDimensions();\n this.afterSetDimensions();\n\n this._maxLength = this.isHorizontal()\n ? this.width + margins.left + margins.right\n : this.height + margins.top + margins.bottom;\n\n // Data min/max\n if (!this._dataLimitsCached) {\n this.beforeDataLimits();\n this.determineDataLimits();\n this.afterDataLimits();\n this._range = _addGrace(this, grace, beginAtZero);\n this._dataLimitsCached = true;\n }\n\n this.beforeBuildTicks();\n\n this.ticks = this.buildTicks() || [];\n\n // Allow modification of ticks in callback.\n this.afterBuildTicks();\n\n // Compute tick rotation and fit using a sampled subset of labels\n // We generally don't need to compute the size of every single label for determining scale size\n const samplingEnabled = sampleSize < this.ticks.length;\n this._convertTicksToLabels(samplingEnabled ? sample(this.ticks, sampleSize) : this.ticks);\n\n // configure is called twice, once here, once from core.controller.updateLayout.\n // Here we haven't been positioned yet, but dimensions are correct.\n // Variables set in configure are needed for calculateLabelRotation, and\n // it's ok that coordinates are not correct there, only dimensions matter.\n this.configure();\n\n // Tick Rotation\n this.beforeCalculateLabelRotation();\n this.calculateLabelRotation(); // Preconditions: number of ticks and sizes of largest labels must be calculated beforehand\n this.afterCalculateLabelRotation();\n\n // Auto-skip\n if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) {\n this.ticks = autoSkip(this, this.ticks);\n this._labelSizes = null;\n this.afterAutoSkip();\n }\n\n if (samplingEnabled) {\n // Generate labels using all non-skipped ticks\n this._convertTicksToLabels(this.ticks);\n }\n\n this.beforeFit();\n this.fit(); // Preconditions: label rotation and label sizes must be calculated beforehand\n this.afterFit();\n\n // IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!\n\n this.afterUpdate();\n }\n\n /**\n\t * @protected\n\t */\n configure() {\n let reversePixels = this.options.reverse;\n let startPixel, endPixel;\n\n if (this.isHorizontal()) {\n startPixel = this.left;\n endPixel = this.right;\n } else {\n startPixel = this.top;\n endPixel = this.bottom;\n // by default vertical scales are from bottom to top, so pixels are reversed\n reversePixels = !reversePixels;\n }\n this._startPixel = startPixel;\n this._endPixel = endPixel;\n this._reversePixels = reversePixels;\n this._length = endPixel - startPixel;\n this._alignToPixels = this.options.alignToPixels;\n }\n\n afterUpdate() {\n call(this.options.afterUpdate, [this]);\n }\n\n //\n\n beforeSetDimensions() {\n call(this.options.beforeSetDimensions, [this]);\n }\n setDimensions() {\n // Set the unconstrained dimension before label rotation\n if (this.isHorizontal()) {\n // Reset position before calculating rotation\n this.width = this.maxWidth;\n this.left = 0;\n this.right = this.width;\n } else {\n this.height = this.maxHeight;\n\n // Reset position before calculating rotation\n this.top = 0;\n this.bottom = this.height;\n }\n\n // Reset padding\n this.paddingLeft = 0;\n this.paddingTop = 0;\n this.paddingRight = 0;\n this.paddingBottom = 0;\n }\n afterSetDimensions() {\n call(this.options.afterSetDimensions, [this]);\n }\n\n _callHooks(name) {\n this.chart.notifyPlugins(name, this.getContext());\n call(this.options[name], [this]);\n }\n\n // Data limits\n beforeDataLimits() {\n this._callHooks('beforeDataLimits');\n }\n determineDataLimits() {}\n afterDataLimits() {\n this._callHooks('afterDataLimits');\n }\n\n //\n beforeBuildTicks() {\n this._callHooks('beforeBuildTicks');\n }\n /**\n\t * @return {object[]} the ticks\n\t */\n buildTicks() {\n return [];\n }\n afterBuildTicks() {\n this._callHooks('afterBuildTicks');\n }\n\n beforeTickToLabelConversion() {\n call(this.options.beforeTickToLabelConversion, [this]);\n }\n /**\n\t * Convert ticks to label strings\n\t * @param {Tick[]} ticks\n\t */\n generateTickLabels(ticks) {\n const tickOpts = this.options.ticks;\n let i, ilen, tick;\n for (i = 0, ilen = ticks.length; i < ilen; i++) {\n tick = ticks[i];\n tick.label = call(tickOpts.callback, [tick.value, i, ticks], this);\n }\n }\n afterTickToLabelConversion() {\n call(this.options.afterTickToLabelConversion, [this]);\n }\n\n //\n\n beforeCalculateLabelRotation() {\n call(this.options.beforeCalculateLabelRotation, [this]);\n }\n calculateLabelRotation() {\n const options = this.options;\n const tickOpts = options.ticks;\n const numTicks = getTicksLimit(this.ticks.length, options.ticks.maxTicksLimit);\n const minRotation = tickOpts.minRotation || 0;\n const maxRotation = tickOpts.maxRotation;\n let labelRotation = minRotation;\n let tickWidth, maxHeight, maxLabelDiagonal;\n\n if (!this._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !this.isHorizontal()) {\n this.labelRotation = minRotation;\n return;\n }\n\n const labelSizes = this._getLabelSizes();\n const maxLabelWidth = labelSizes.widest.width;\n const maxLabelHeight = labelSizes.highest.height;\n\n // Estimate the width of each grid based on the canvas width, the maximum\n // label width and the number of tick intervals\n const maxWidth = _limitValue(this.chart.width - maxLabelWidth, 0, this.maxWidth);\n tickWidth = options.offset ? this.maxWidth / numTicks : maxWidth / (numTicks - 1);\n\n // Allow 3 pixels x2 padding either side for label readability\n if (maxLabelWidth + 6 > tickWidth) {\n tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1));\n maxHeight = this.maxHeight - getTickMarkLength(options.grid)\n\t\t\t\t- tickOpts.padding - getTitleHeight(options.title, this.chart.options.font);\n maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight);\n labelRotation = toDegrees(Math.min(\n Math.asin(_limitValue((labelSizes.highest.height + 6) / tickWidth, -1, 1)),\n Math.asin(_limitValue(maxHeight / maxLabelDiagonal, -1, 1)) - Math.asin(_limitValue(maxLabelHeight / maxLabelDiagonal, -1, 1))\n ));\n labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation));\n }\n\n this.labelRotation = labelRotation;\n }\n afterCalculateLabelRotation() {\n call(this.options.afterCalculateLabelRotation, [this]);\n }\n afterAutoSkip() {}\n\n //\n\n beforeFit() {\n call(this.options.beforeFit, [this]);\n }\n fit() {\n // Reset\n const minSize = {\n width: 0,\n height: 0\n };\n\n const {chart, options: {ticks: tickOpts, title: titleOpts, grid: gridOpts}} = this;\n const display = this._isVisible();\n const isHorizontal = this.isHorizontal();\n\n if (display) {\n const titleHeight = getTitleHeight(titleOpts, chart.options.font);\n if (isHorizontal) {\n minSize.width = this.maxWidth;\n minSize.height = getTickMarkLength(gridOpts) + titleHeight;\n } else {\n minSize.height = this.maxHeight; // fill all the height\n minSize.width = getTickMarkLength(gridOpts) + titleHeight;\n }\n\n // Don't bother fitting the ticks if we are not showing the labels\n if (tickOpts.display && this.ticks.length) {\n const {first, last, widest, highest} = this._getLabelSizes();\n const tickPadding = tickOpts.padding * 2;\n const angleRadians = toRadians(this.labelRotation);\n const cos = Math.cos(angleRadians);\n const sin = Math.sin(angleRadians);\n\n if (isHorizontal) {\n // A horizontal axis is more constrained by the height.\n const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height;\n minSize.height = Math.min(this.maxHeight, minSize.height + labelHeight + tickPadding);\n } else {\n // A vertical axis is more constrained by the width. Labels are the\n // dominant factor here, so get that length first and account for padding\n const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height;\n\n minSize.width = Math.min(this.maxWidth, minSize.width + labelWidth + tickPadding);\n }\n this._calculatePadding(first, last, sin, cos);\n }\n }\n\n this._handleMargins();\n\n if (isHorizontal) {\n this.width = this._length = chart.width - this._margins.left - this._margins.right;\n this.height = minSize.height;\n } else {\n this.width = minSize.width;\n this.height = this._length = chart.height - this._margins.top - this._margins.bottom;\n }\n }\n\n _calculatePadding(first, last, sin, cos) {\n const {ticks: {align, padding}, position} = this.options;\n const isRotated = this.labelRotation !== 0;\n const labelsBelowTicks = position !== 'top' && this.axis === 'x';\n\n if (this.isHorizontal()) {\n const offsetLeft = this.getPixelForTick(0) - this.left;\n const offsetRight = this.right - this.getPixelForTick(this.ticks.length - 1);\n let paddingLeft = 0;\n let paddingRight = 0;\n\n // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned\n // which means that the right padding is dominated by the font height\n if (isRotated) {\n if (labelsBelowTicks) {\n paddingLeft = cos * first.width;\n paddingRight = sin * last.height;\n } else {\n paddingLeft = sin * first.height;\n paddingRight = cos * last.width;\n }\n } else if (align === 'start') {\n paddingRight = last.width;\n } else if (align === 'end') {\n paddingLeft = first.width;\n } else if (align !== 'inner') {\n paddingLeft = first.width / 2;\n paddingRight = last.width / 2;\n }\n\n // Adjust padding taking into account changes in offsets\n this.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * this.width / (this.width - offsetLeft), 0);\n this.paddingRight = Math.max((paddingRight - offsetRight + padding) * this.width / (this.width - offsetRight), 0);\n } else {\n let paddingTop = last.height / 2;\n let paddingBottom = first.height / 2;\n\n if (align === 'start') {\n paddingTop = 0;\n paddingBottom = first.height;\n } else if (align === 'end') {\n paddingTop = last.height;\n paddingBottom = 0;\n }\n\n this.paddingTop = paddingTop + padding;\n this.paddingBottom = paddingBottom + padding;\n }\n }\n\n /**\n\t * Handle margins and padding interactions\n\t * @private\n\t */\n _handleMargins() {\n if (this._margins) {\n this._margins.left = Math.max(this.paddingLeft, this._margins.left);\n this._margins.top = Math.max(this.paddingTop, this._margins.top);\n this._margins.right = Math.max(this.paddingRight, this._margins.right);\n this._margins.bottom = Math.max(this.paddingBottom, this._margins.bottom);\n }\n }\n\n afterFit() {\n call(this.options.afterFit, [this]);\n }\n\n // Shared Methods\n /**\n\t * @return {boolean}\n\t */\n isHorizontal() {\n const {axis, position} = this.options;\n return position === 'top' || position === 'bottom' || axis === 'x';\n }\n /**\n\t * @return {boolean}\n\t */\n isFullSize() {\n return this.options.fullSize;\n }\n\n /**\n\t * @param {Tick[]} ticks\n\t * @private\n\t */\n _convertTicksToLabels(ticks) {\n this.beforeTickToLabelConversion();\n\n this.generateTickLabels(ticks);\n\n // Ticks should be skipped when callback returns null or undef, so lets remove those.\n let i, ilen;\n for (i = 0, ilen = ticks.length; i < ilen; i++) {\n if (isNullOrUndef(ticks[i].label)) {\n ticks.splice(i, 1);\n ilen--;\n i--;\n }\n }\n\n this.afterTickToLabelConversion();\n }\n\n /**\n\t * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }}\n\t * @private\n\t */\n _getLabelSizes() {\n let labelSizes = this._labelSizes;\n\n if (!labelSizes) {\n const sampleSize = this.options.ticks.sampleSize;\n let ticks = this.ticks;\n if (sampleSize < ticks.length) {\n ticks = sample(ticks, sampleSize);\n }\n\n this._labelSizes = labelSizes = this._computeLabelSizes(ticks, ticks.length, this.options.ticks.maxTicksLimit);\n }\n\n return labelSizes;\n }\n\n /**\n\t * Returns {width, height, offset} objects for the first, last, widest, highest tick\n\t * labels where offset indicates the anchor point offset from the top in pixels.\n\t * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }}\n\t * @private\n\t */\n _computeLabelSizes(ticks, length, maxTicksLimit) {\n const {ctx, _longestTextCache: caches} = this;\n const widths = [];\n const heights = [];\n const increment = Math.floor(length / getTicksLimit(length, maxTicksLimit));\n let widestLabelSize = 0;\n let highestLabelSize = 0;\n let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel;\n\n for (i = 0; i < length; i += increment) {\n label = ticks[i].label;\n tickFont = this._resolveTickFontOptions(i);\n ctx.font = fontString = tickFont.string;\n cache = caches[fontString] = caches[fontString] || {data: {}, gc: []};\n lineHeight = tickFont.lineHeight;\n width = height = 0;\n // Undefined labels and arrays should not be measured\n if (!isNullOrUndef(label) && !isArray(label)) {\n width = _measureText(ctx, cache.data, cache.gc, width, label);\n height = lineHeight;\n } else if (isArray(label)) {\n // if it is an array let's measure each element\n for (j = 0, jlen = label.length; j < jlen; ++j) {\n nestedLabel = /** @type {string} */ (label[j]);\n // Undefined labels and arrays should not be measured\n if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) {\n width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel);\n height += lineHeight;\n }\n }\n }\n widths.push(width);\n heights.push(height);\n widestLabelSize = Math.max(width, widestLabelSize);\n highestLabelSize = Math.max(height, highestLabelSize);\n }\n garbageCollect(caches, length);\n\n const widest = widths.indexOf(widestLabelSize);\n const highest = heights.indexOf(highestLabelSize);\n\n const valueAt = (idx) => ({width: widths[idx] || 0, height: heights[idx] || 0});\n\n return {\n first: valueAt(0),\n last: valueAt(length - 1),\n widest: valueAt(widest),\n highest: valueAt(highest),\n widths,\n heights,\n };\n }\n\n /**\n\t * Used to get the label to display in the tooltip for the given value\n\t * @param {*} value\n\t * @return {string}\n\t */\n getLabelForValue(value) {\n return value;\n }\n\n /**\n\t * Returns the location of the given data point. Value can either be an index or a numerical value\n\t * The coordinate (0, 0) is at the upper-left corner of the canvas\n\t * @param {*} value\n\t * @param {number} [index]\n\t * @return {number}\n\t */\n getPixelForValue(value, index) { // eslint-disable-line no-unused-vars\n return NaN;\n }\n\n /**\n\t * Used to get the data value from a given pixel. This is the inverse of getPixelForValue\n\t * The coordinate (0, 0) is at the upper-left corner of the canvas\n\t * @param {number} pixel\n\t * @return {*}\n\t */\n getValueForPixel(pixel) {} // eslint-disable-line no-unused-vars\n\n /**\n\t * Returns the location of the tick at the given index\n\t * The coordinate (0, 0) is at the upper-left corner of the canvas\n\t * @param {number} index\n\t * @return {number}\n\t */\n getPixelForTick(index) {\n const ticks = this.ticks;\n if (index < 0 || index > ticks.length - 1) {\n return null;\n }\n return this.getPixelForValue(ticks[index].value);\n }\n\n /**\n\t * Utility for getting the pixel location of a percentage of scale\n\t * The coordinate (0, 0) is at the upper-left corner of the canvas\n\t * @param {number} decimal\n\t * @return {number}\n\t */\n getPixelForDecimal(decimal) {\n if (this._reversePixels) {\n decimal = 1 - decimal;\n }\n\n const pixel = this._startPixel + decimal * this._length;\n return _int16Range(this._alignToPixels ? _alignPixel(this.chart, pixel, 0) : pixel);\n }\n\n /**\n\t * @param {number} pixel\n\t * @return {number}\n\t */\n getDecimalForPixel(pixel) {\n const decimal = (pixel - this._startPixel) / this._length;\n return this._reversePixels ? 1 - decimal : decimal;\n }\n\n /**\n\t * Returns the pixel for the minimum chart value\n\t * The coordinate (0, 0) is at the upper-left corner of the canvas\n\t * @return {number}\n\t */\n getBasePixel() {\n return this.getPixelForValue(this.getBaseValue());\n }\n\n /**\n\t * @return {number}\n\t */\n getBaseValue() {\n const {min, max} = this;\n\n return min < 0 && max < 0 ? max :\n min > 0 && max > 0 ? min :\n 0;\n }\n\n /**\n\t * @protected\n\t */\n getContext(index) {\n const ticks = this.ticks || [];\n\n if (index >= 0 && index < ticks.length) {\n const tick = ticks[index];\n return tick.$context ||\n\t\t\t\t(tick.$context = createTickContext(this.getContext(), index, tick));\n }\n return this.$context ||\n\t\t\t(this.$context = createScaleContext(this.chart.getContext(), this));\n }\n\n /**\n\t * @return {number}\n\t * @private\n\t */\n _tickSize() {\n const optionTicks = this.options.ticks;\n\n // Calculate space needed by label in axis direction.\n const rot = toRadians(this.labelRotation);\n const cos = Math.abs(Math.cos(rot));\n const sin = Math.abs(Math.sin(rot));\n\n const labelSizes = this._getLabelSizes();\n const padding = optionTicks.autoSkipPadding || 0;\n const w = labelSizes ? labelSizes.widest.width + padding : 0;\n const h = labelSizes ? labelSizes.highest.height + padding : 0;\n\n // Calculate space needed for 1 tick in axis direction.\n return this.isHorizontal()\n ? h * cos > w * sin ? w / cos : h / sin\n : h * sin < w * cos ? h / cos : w / sin;\n }\n\n /**\n\t * @return {boolean}\n\t * @private\n\t */\n _isVisible() {\n const display = this.options.display;\n\n if (display !== 'auto') {\n return !!display;\n }\n\n return this.getMatchingVisibleMetas().length > 0;\n }\n\n /**\n\t * @private\n\t */\n _computeGridLineItems(chartArea) {\n const axis = this.axis;\n const chart = this.chart;\n const options = this.options;\n const {grid, position, border} = options;\n const offset = grid.offset;\n const isHorizontal = this.isHorizontal();\n const ticks = this.ticks;\n const ticksLength = ticks.length + (offset ? 1 : 0);\n const tl = getTickMarkLength(grid);\n const items = [];\n\n const borderOpts = border.setContext(this.getContext());\n const axisWidth = borderOpts.display ? borderOpts.width : 0;\n const axisHalfWidth = axisWidth / 2;\n const alignBorderValue = function(pixel) {\n return _alignPixel(chart, pixel, axisWidth);\n };\n let borderValue, i, lineValue, alignedLineValue;\n let tx1, ty1, tx2, ty2, x1, y1, x2, y2;\n\n if (position === 'top') {\n borderValue = alignBorderValue(this.bottom);\n ty1 = this.bottom - tl;\n ty2 = borderValue - axisHalfWidth;\n y1 = alignBorderValue(chartArea.top) + axisHalfWidth;\n y2 = chartArea.bottom;\n } else if (position === 'bottom') {\n borderValue = alignBorderValue(this.top);\n y1 = chartArea.top;\n y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth;\n ty1 = borderValue + axisHalfWidth;\n ty2 = this.top + tl;\n } else if (position === 'left') {\n borderValue = alignBorderValue(this.right);\n tx1 = this.right - tl;\n tx2 = borderValue - axisHalfWidth;\n x1 = alignBorderValue(chartArea.left) + axisHalfWidth;\n x2 = chartArea.right;\n } else if (position === 'right') {\n borderValue = alignBorderValue(this.left);\n x1 = chartArea.left;\n x2 = alignBorderValue(chartArea.right) - axisHalfWidth;\n tx1 = borderValue + axisHalfWidth;\n tx2 = this.left + tl;\n } else if (axis === 'x') {\n if (position === 'center') {\n borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5);\n } else if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));\n }\n\n y1 = chartArea.top;\n y2 = chartArea.bottom;\n ty1 = borderValue + axisHalfWidth;\n ty2 = ty1 + tl;\n } else if (axis === 'y') {\n if (position === 'center') {\n borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2);\n } else if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value));\n }\n\n tx1 = borderValue - axisHalfWidth;\n tx2 = tx1 - tl;\n x1 = chartArea.left;\n x2 = chartArea.right;\n }\n\n const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength);\n const step = Math.max(1, Math.ceil(ticksLength / limit));\n for (i = 0; i < ticksLength; i += step) {\n const context = this.getContext(i);\n const optsAtIndex = grid.setContext(context);\n const optsAtIndexBorder = border.setContext(context);\n\n const lineWidth = optsAtIndex.lineWidth;\n const lineColor = optsAtIndex.color;\n const borderDash = optsAtIndexBorder.dash || [];\n const borderDashOffset = optsAtIndexBorder.dashOffset;\n\n const tickWidth = optsAtIndex.tickWidth;\n const tickColor = optsAtIndex.tickColor;\n const tickBorderDash = optsAtIndex.tickBorderDash || [];\n const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset;\n\n lineValue = getPixelForGridLine(this, i, offset);\n\n // Skip if the pixel is out of the range\n if (lineValue === undefined) {\n continue;\n }\n\n alignedLineValue = _alignPixel(chart, lineValue, lineWidth);\n\n if (isHorizontal) {\n tx1 = tx2 = x1 = x2 = alignedLineValue;\n } else {\n ty1 = ty2 = y1 = y2 = alignedLineValue;\n }\n\n items.push({\n tx1,\n ty1,\n tx2,\n ty2,\n x1,\n y1,\n x2,\n y2,\n width: lineWidth,\n color: lineColor,\n borderDash,\n borderDashOffset,\n tickWidth,\n tickColor,\n tickBorderDash,\n tickBorderDashOffset,\n });\n }\n\n this._ticksLength = ticksLength;\n this._borderValue = borderValue;\n\n return items;\n }\n\n /**\n\t * @private\n\t */\n _computeLabelItems(chartArea) {\n const axis = this.axis;\n const options = this.options;\n const {position, ticks: optionTicks} = options;\n const isHorizontal = this.isHorizontal();\n const ticks = this.ticks;\n const {align, crossAlign, padding, mirror} = optionTicks;\n const tl = getTickMarkLength(options.grid);\n const tickAndPadding = tl + padding;\n const hTickAndPadding = mirror ? -padding : tickAndPadding;\n const rotation = -toRadians(this.labelRotation);\n const items = [];\n let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset;\n let textBaseline = 'middle';\n\n if (position === 'top') {\n y = this.bottom - hTickAndPadding;\n textAlign = this._getXAxisLabelAlignment();\n } else if (position === 'bottom') {\n y = this.top + hTickAndPadding;\n textAlign = this._getXAxisLabelAlignment();\n } else if (position === 'left') {\n const ret = this._getYAxisLabelAlignment(tl);\n textAlign = ret.textAlign;\n x = ret.x;\n } else if (position === 'right') {\n const ret = this._getYAxisLabelAlignment(tl);\n textAlign = ret.textAlign;\n x = ret.x;\n } else if (axis === 'x') {\n if (position === 'center') {\n y = ((chartArea.top + chartArea.bottom) / 2) + tickAndPadding;\n } else if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n y = this.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding;\n }\n textAlign = this._getXAxisLabelAlignment();\n } else if (axis === 'y') {\n if (position === 'center') {\n x = ((chartArea.left + chartArea.right) / 2) - tickAndPadding;\n } else if (isObject(position)) {\n const positionAxisID = Object.keys(position)[0];\n const value = position[positionAxisID];\n x = this.chart.scales[positionAxisID].getPixelForValue(value);\n }\n textAlign = this._getYAxisLabelAlignment(tl).textAlign;\n }\n\n if (axis === 'y') {\n if (align === 'start') {\n textBaseline = 'top';\n } else if (align === 'end') {\n textBaseline = 'bottom';\n }\n }\n\n const labelSizes = this._getLabelSizes();\n for (i = 0, ilen = ticks.length; i < ilen; ++i) {\n tick = ticks[i];\n label = tick.label;\n\n const optsAtIndex = optionTicks.setContext(this.getContext(i));\n pixel = this.getPixelForTick(i) + optionTicks.labelOffset;\n font = this._resolveTickFontOptions(i);\n lineHeight = font.lineHeight;\n lineCount = isArray(label) ? label.length : 1;\n const halfCount = lineCount / 2;\n const color = optsAtIndex.color;\n const strokeColor = optsAtIndex.textStrokeColor;\n const strokeWidth = optsAtIndex.textStrokeWidth;\n let tickTextAlign = textAlign;\n\n if (isHorizontal) {\n x = pixel;\n\n if (textAlign === 'inner') {\n if (i === ilen - 1) {\n tickTextAlign = !this.options.reverse ? 'right' : 'left';\n } else if (i === 0) {\n tickTextAlign = !this.options.reverse ? 'left' : 'right';\n } else {\n tickTextAlign = 'center';\n }\n }\n\n if (position === 'top') {\n if (crossAlign === 'near' || rotation !== 0) {\n textOffset = -lineCount * lineHeight + lineHeight / 2;\n } else if (crossAlign === 'center') {\n textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight;\n } else {\n textOffset = -labelSizes.highest.height + lineHeight / 2;\n }\n } else {\n // eslint-disable-next-line no-lonely-if\n if (crossAlign === 'near' || rotation !== 0) {\n textOffset = lineHeight / 2;\n } else if (crossAlign === 'center') {\n textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight;\n } else {\n textOffset = labelSizes.highest.height - lineCount * lineHeight;\n }\n }\n if (mirror) {\n textOffset *= -1;\n }\n if (rotation !== 0 && !optsAtIndex.showLabelBackdrop) {\n x += (lineHeight / 2) * Math.sin(rotation);\n }\n } else {\n y = pixel;\n textOffset = (1 - lineCount) * lineHeight / 2;\n }\n\n let backdrop;\n\n if (optsAtIndex.showLabelBackdrop) {\n const labelPadding = toPadding(optsAtIndex.backdropPadding);\n const height = labelSizes.heights[i];\n const width = labelSizes.widths[i];\n\n let top = textOffset - labelPadding.top;\n let left = 0 - labelPadding.left;\n\n switch (textBaseline) {\n case 'middle':\n top -= height / 2;\n break;\n case 'bottom':\n top -= height;\n break;\n default:\n break;\n }\n\n switch (textAlign) {\n case 'center':\n left -= width / 2;\n break;\n case 'right':\n left -= width;\n break;\n case 'inner':\n if (i === ilen - 1) {\n left -= width;\n } else if (i > 0) {\n left -= width / 2;\n }\n break;\n default:\n break;\n }\n\n backdrop = {\n left,\n top,\n width: width + labelPadding.width,\n height: height + labelPadding.height,\n\n color: optsAtIndex.backdropColor,\n };\n }\n\n items.push({\n label,\n font,\n textOffset,\n options: {\n rotation,\n color,\n strokeColor,\n strokeWidth,\n textAlign: tickTextAlign,\n textBaseline,\n translation: [x, y],\n backdrop,\n }\n });\n }\n\n return items;\n }\n\n _getXAxisLabelAlignment() {\n const {position, ticks} = this.options;\n const rotation = -toRadians(this.labelRotation);\n\n if (rotation) {\n return position === 'top' ? 'left' : 'right';\n }\n\n let align = 'center';\n\n if (ticks.align === 'start') {\n align = 'left';\n } else if (ticks.align === 'end') {\n align = 'right';\n } else if (ticks.align === 'inner') {\n align = 'inner';\n }\n\n return align;\n }\n\n _getYAxisLabelAlignment(tl) {\n const {position, ticks: {crossAlign, mirror, padding}} = this.options;\n const labelSizes = this._getLabelSizes();\n const tickAndPadding = tl + padding;\n const widest = labelSizes.widest.width;\n\n let textAlign;\n let x;\n\n if (position === 'left') {\n if (mirror) {\n x = this.right + padding;\n\n if (crossAlign === 'near') {\n textAlign = 'left';\n } else if (crossAlign === 'center') {\n textAlign = 'center';\n x += (widest / 2);\n } else {\n textAlign = 'right';\n x += widest;\n }\n } else {\n x = this.right - tickAndPadding;\n\n if (crossAlign === 'near') {\n textAlign = 'right';\n } else if (crossAlign === 'center') {\n textAlign = 'center';\n x -= (widest / 2);\n } else {\n textAlign = 'left';\n x = this.left;\n }\n }\n } else if (position === 'right') {\n if (mirror) {\n x = this.left + padding;\n\n if (crossAlign === 'near') {\n textAlign = 'right';\n } else if (crossAlign === 'center') {\n textAlign = 'center';\n x -= (widest / 2);\n } else {\n textAlign = 'left';\n x -= widest;\n }\n } else {\n x = this.left + tickAndPadding;\n\n if (crossAlign === 'near') {\n textAlign = 'left';\n } else if (crossAlign === 'center') {\n textAlign = 'center';\n x += widest / 2;\n } else {\n textAlign = 'right';\n x = this.right;\n }\n }\n } else {\n textAlign = 'right';\n }\n\n return {textAlign, x};\n }\n\n /**\n\t * @private\n\t */\n _computeLabelArea() {\n if (this.options.ticks.mirror) {\n return;\n }\n\n const chart = this.chart;\n const position = this.options.position;\n\n if (position === 'left' || position === 'right') {\n return {top: 0, left: this.left, bottom: chart.height, right: this.right};\n } if (position === 'top' || position === 'bottom') {\n return {top: this.top, left: 0, bottom: this.bottom, right: chart.width};\n }\n }\n\n /**\n * @protected\n */\n drawBackground() {\n const {ctx, options: {backgroundColor}, left, top, width, height} = this;\n if (backgroundColor) {\n ctx.save();\n ctx.fillStyle = backgroundColor;\n ctx.fillRect(left, top, width, height);\n ctx.restore();\n }\n }\n\n getLineWidthForValue(value) {\n const grid = this.options.grid;\n if (!this._isVisible() || !grid.display) {\n return 0;\n }\n const ticks = this.ticks;\n const index = ticks.findIndex(t => t.value === value);\n if (index >= 0) {\n const opts = grid.setContext(this.getContext(index));\n return opts.lineWidth;\n }\n return 0;\n }\n\n /**\n\t * @protected\n\t */\n drawGrid(chartArea) {\n const grid = this.options.grid;\n const ctx = this.ctx;\n const items = this._gridLineItems || (this._gridLineItems = this._computeGridLineItems(chartArea));\n let i, ilen;\n\n const drawLine = (p1, p2, style) => {\n if (!style.width || !style.color) {\n return;\n }\n ctx.save();\n ctx.lineWidth = style.width;\n ctx.strokeStyle = style.color;\n ctx.setLineDash(style.borderDash || []);\n ctx.lineDashOffset = style.borderDashOffset;\n\n ctx.beginPath();\n ctx.moveTo(p1.x, p1.y);\n ctx.lineTo(p2.x, p2.y);\n ctx.stroke();\n ctx.restore();\n };\n\n if (grid.display) {\n for (i = 0, ilen = items.length; i < ilen; ++i) {\n const item = items[i];\n\n if (grid.drawOnChartArea) {\n drawLine(\n {x: item.x1, y: item.y1},\n {x: item.x2, y: item.y2},\n item\n );\n }\n\n if (grid.drawTicks) {\n drawLine(\n {x: item.tx1, y: item.ty1},\n {x: item.tx2, y: item.ty2},\n {\n color: item.tickColor,\n width: item.tickWidth,\n borderDash: item.tickBorderDash,\n borderDashOffset: item.tickBorderDashOffset\n }\n );\n }\n }\n }\n }\n\n /**\n\t * @protected\n\t */\n drawBorder() {\n const {chart, ctx, options: {border, grid}} = this;\n const borderOpts = border.setContext(this.getContext());\n const axisWidth = border.display ? borderOpts.width : 0;\n if (!axisWidth) {\n return;\n }\n const lastLineWidth = grid.setContext(this.getContext(0)).lineWidth;\n const borderValue = this._borderValue;\n let x1, x2, y1, y2;\n\n if (this.isHorizontal()) {\n x1 = _alignPixel(chart, this.left, axisWidth) - axisWidth / 2;\n x2 = _alignPixel(chart, this.right, lastLineWidth) + lastLineWidth / 2;\n y1 = y2 = borderValue;\n } else {\n y1 = _alignPixel(chart, this.top, axisWidth) - axisWidth / 2;\n y2 = _alignPixel(chart, this.bottom, lastLineWidth) + lastLineWidth / 2;\n x1 = x2 = borderValue;\n }\n ctx.save();\n ctx.lineWidth = borderOpts.width;\n ctx.strokeStyle = borderOpts.color;\n\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n\n ctx.restore();\n }\n\n /**\n\t * @protected\n\t */\n drawLabels(chartArea) {\n const optionTicks = this.options.ticks;\n\n if (!optionTicks.display) {\n return;\n }\n\n const ctx = this.ctx;\n\n const area = this._computeLabelArea();\n if (area) {\n clipArea(ctx, area);\n }\n\n const items = this.getLabelItems(chartArea);\n for (const item of items) {\n const renderTextOptions = item.options;\n const tickFont = item.font;\n const label = item.label;\n const y = item.textOffset;\n renderText(ctx, label, 0, y, tickFont, renderTextOptions);\n }\n\n if (area) {\n unclipArea(ctx);\n }\n }\n\n /**\n\t * @protected\n\t */\n drawTitle() {\n const {ctx, options: {position, title, reverse}} = this;\n\n if (!title.display) {\n return;\n }\n\n const font = toFont(title.font);\n const padding = toPadding(title.padding);\n const align = title.align;\n let offset = font.lineHeight / 2;\n\n if (position === 'bottom' || position === 'center' || isObject(position)) {\n offset += padding.bottom;\n if (isArray(title.text)) {\n offset += font.lineHeight * (title.text.length - 1);\n }\n } else {\n offset += padding.top;\n }\n\n const {titleX, titleY, maxWidth, rotation} = titleArgs(this, offset, position, align);\n\n renderText(ctx, title.text, 0, 0, font, {\n color: title.color,\n maxWidth,\n rotation,\n textAlign: titleAlign(align, position, reverse),\n textBaseline: 'middle',\n translation: [titleX, titleY],\n });\n }\n\n draw(chartArea) {\n if (!this._isVisible()) {\n return;\n }\n\n this.drawBackground();\n this.drawGrid(chartArea);\n this.drawBorder();\n this.drawTitle();\n this.drawLabels(chartArea);\n }\n\n /**\n\t * @return {object[]}\n\t * @private\n\t */\n _layers() {\n const opts = this.options;\n const tz = opts.ticks && opts.ticks.z || 0;\n const gz = valueOrDefault(opts.grid && opts.grid.z, -1);\n const bz = valueOrDefault(opts.border && opts.border.z, 0);\n\n if (!this._isVisible() || this.draw !== Scale.prototype.draw) {\n // backward compatibility: draw has been overridden by custom scale\n return [{\n z: tz,\n draw: (chartArea) => {\n this.draw(chartArea);\n }\n }];\n }\n\n return [{\n z: gz,\n draw: (chartArea) => {\n this.drawBackground();\n this.drawGrid(chartArea);\n this.drawTitle();\n }\n }, {\n z: bz,\n draw: () => {\n this.drawBorder();\n }\n }, {\n z: tz,\n draw: (chartArea) => {\n this.drawLabels(chartArea);\n }\n }];\n }\n\n /**\n\t * Returns visible dataset metas that are attached to this scale\n\t * @param {string} [type] - if specified, also filter by dataset type\n\t * @return {object[]}\n\t */\n getMatchingVisibleMetas(type) {\n const metas = this.chart.getSortedVisibleDatasetMetas();\n const axisID = this.axis + 'AxisID';\n const result = [];\n let i, ilen;\n\n for (i = 0, ilen = metas.length; i < ilen; ++i) {\n const meta = metas[i];\n if (meta[axisID] === this.id && (!type || meta.type === type)) {\n result.push(meta);\n }\n }\n return result;\n }\n\n /**\n\t * @param {number} index\n\t * @return {object}\n\t * @protected\n \t */\n _resolveTickFontOptions(index) {\n const opts = this.options.ticks.setContext(this.getContext(index));\n return toFont(opts.font);\n }\n\n /**\n * @protected\n */\n _maxDigits() {\n const fontSize = this._resolveTickFontOptions(0).lineHeight;\n return (this.isHorizontal() ? this.width : this.height) / fontSize;\n }\n}\n","import {merge} from '../helpers/index.js';\nimport defaults, {overrides} from './core.defaults.js';\n\n/**\n * @typedef {{id: string, defaults: any, overrides?: any, defaultRoutes: any}} IChartComponent\n */\n\nexport default class TypedRegistry {\n constructor(type, scope, override) {\n this.type = type;\n this.scope = scope;\n this.override = override;\n this.items = Object.create(null);\n }\n\n isForType(type) {\n return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype);\n }\n\n /**\n\t * @param {IChartComponent} item\n\t * @returns {string} The scope where items defaults were registered to.\n\t */\n register(item) {\n const proto = Object.getPrototypeOf(item);\n let parentScope;\n\n if (isIChartComponent(proto)) {\n // Make sure the parent is registered and note the scope where its defaults are.\n parentScope = this.register(proto);\n }\n\n const items = this.items;\n const id = item.id;\n const scope = this.scope + '.' + id;\n\n if (!id) {\n throw new Error('class does not have id: ' + item);\n }\n\n if (id in items) {\n // already registered\n return scope;\n }\n\n items[id] = item;\n registerDefaults(item, scope, parentScope);\n if (this.override) {\n defaults.override(item.id, item.overrides);\n }\n\n return scope;\n }\n\n /**\n\t * @param {string} id\n\t * @returns {object?}\n\t */\n get(id) {\n return this.items[id];\n }\n\n /**\n\t * @param {IChartComponent} item\n\t */\n unregister(item) {\n const items = this.items;\n const id = item.id;\n const scope = this.scope;\n\n if (id in items) {\n delete items[id];\n }\n\n if (scope && id in defaults[scope]) {\n delete defaults[scope][id];\n if (this.override) {\n delete overrides[id];\n }\n }\n }\n}\n\nfunction registerDefaults(item, scope, parentScope) {\n // Inherit the parent's defaults and keep existing defaults\n const itemDefaults = merge(Object.create(null), [\n parentScope ? defaults.get(parentScope) : {},\n defaults.get(scope),\n item.defaults\n ]);\n\n defaults.set(scope, itemDefaults);\n\n if (item.defaultRoutes) {\n routeDefaults(scope, item.defaultRoutes);\n }\n\n if (item.descriptors) {\n defaults.describe(scope, item.descriptors);\n }\n}\n\nfunction routeDefaults(scope, routes) {\n Object.keys(routes).forEach(property => {\n const propertyParts = property.split('.');\n const sourceName = propertyParts.pop();\n const sourceScope = [scope].concat(propertyParts).join('.');\n const parts = routes[property].split('.');\n const targetName = parts.pop();\n const targetScope = parts.join('.');\n defaults.route(sourceScope, sourceName, targetScope, targetName);\n });\n}\n\nfunction isIChartComponent(proto) {\n return 'id' in proto && 'defaults' in proto;\n}\n","import DatasetController from './core.datasetController.js';\nimport Element from './core.element.js';\nimport Scale from './core.scale.js';\nimport TypedRegistry from './core.typedRegistry.js';\nimport {each, callback as call, _capitalize} from '../helpers/helpers.core.js';\n\n/**\n * Please use the module's default export which provides a singleton instance\n * Note: class is exported for typedoc\n */\nexport class Registry {\n constructor() {\n this.controllers = new TypedRegistry(DatasetController, 'datasets', true);\n this.elements = new TypedRegistry(Element, 'elements');\n this.plugins = new TypedRegistry(Object, 'plugins');\n this.scales = new TypedRegistry(Scale, 'scales');\n // Order is important, Scale has Element in prototype chain,\n // so Scales must be before Elements. Plugins are a fallback, so not listed here.\n this._typedRegistries = [this.controllers, this.scales, this.elements];\n }\n\n /**\n\t * @param {...any} args\n\t */\n add(...args) {\n this._each('register', args);\n }\n\n remove(...args) {\n this._each('unregister', args);\n }\n\n /**\n\t * @param {...typeof DatasetController} args\n\t */\n addControllers(...args) {\n this._each('register', args, this.controllers);\n }\n\n /**\n\t * @param {...typeof Element} args\n\t */\n addElements(...args) {\n this._each('register', args, this.elements);\n }\n\n /**\n\t * @param {...any} args\n\t */\n addPlugins(...args) {\n this._each('register', args, this.plugins);\n }\n\n /**\n\t * @param {...typeof Scale} args\n\t */\n addScales(...args) {\n this._each('register', args, this.scales);\n }\n\n /**\n\t * @param {string} id\n\t * @returns {typeof DatasetController}\n\t */\n getController(id) {\n return this._get(id, this.controllers, 'controller');\n }\n\n /**\n\t * @param {string} id\n\t * @returns {typeof Element}\n\t */\n getElement(id) {\n return this._get(id, this.elements, 'element');\n }\n\n /**\n\t * @param {string} id\n\t * @returns {object}\n\t */\n getPlugin(id) {\n return this._get(id, this.plugins, 'plugin');\n }\n\n /**\n\t * @param {string} id\n\t * @returns {typeof Scale}\n\t */\n getScale(id) {\n return this._get(id, this.scales, 'scale');\n }\n\n /**\n\t * @param {...typeof DatasetController} args\n\t */\n removeControllers(...args) {\n this._each('unregister', args, this.controllers);\n }\n\n /**\n\t * @param {...typeof Element} args\n\t */\n removeElements(...args) {\n this._each('unregister', args, this.elements);\n }\n\n /**\n\t * @param {...any} args\n\t */\n removePlugins(...args) {\n this._each('unregister', args, this.plugins);\n }\n\n /**\n\t * @param {...typeof Scale} args\n\t */\n removeScales(...args) {\n this._each('unregister', args, this.scales);\n }\n\n /**\n\t * @private\n\t */\n _each(method, args, typedRegistry) {\n [...args].forEach(arg => {\n const reg = typedRegistry || this._getRegistryForType(arg);\n if (typedRegistry || reg.isForType(arg) || (reg === this.plugins && arg.id)) {\n this._exec(method, reg, arg);\n } else {\n // Handle loopable args\n // Use case:\n // import * as plugins from './plugins.js';\n // Chart.register(plugins);\n each(arg, item => {\n // If there are mixed types in the loopable, make sure those are\n // registered in correct registry\n // Use case: (treemap exporting controller, elements etc)\n // import * as treemap from 'chartjs-chart-treemap.js';\n // Chart.register(treemap);\n\n const itemReg = typedRegistry || this._getRegistryForType(item);\n this._exec(method, itemReg, item);\n });\n }\n });\n }\n\n /**\n\t * @private\n\t */\n _exec(method, registry, component) {\n const camelMethod = _capitalize(method);\n call(component['before' + camelMethod], [], component); // beforeRegister / beforeUnregister\n registry[method](component);\n call(component['after' + camelMethod], [], component); // afterRegister / afterUnregister\n }\n\n /**\n\t * @private\n\t */\n _getRegistryForType(type) {\n for (let i = 0; i < this._typedRegistries.length; i++) {\n const reg = this._typedRegistries[i];\n if (reg.isForType(type)) {\n return reg;\n }\n }\n // plugins is the fallback registry\n return this.plugins;\n }\n\n /**\n\t * @private\n\t */\n _get(id, typedRegistry, type) {\n const item = typedRegistry.get(id);\n if (item === undefined) {\n throw new Error('\"' + id + '\" is not a registered ' + type + '.');\n }\n return item;\n }\n\n}\n\n// singleton instance\nexport default /* #__PURE__ */ new Registry();\n","import registry from './core.registry.js';\nimport {callback as callCallback, isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js';\n\n/**\n * @typedef { import('./core.controller.js').default } Chart\n * @typedef { import('../types/index.js').ChartEvent } ChartEvent\n * @typedef { import('../plugins/plugin.tooltip.js').default } Tooltip\n */\n\n/**\n * @callback filterCallback\n * @param {{plugin: object, options: object}} value\n * @param {number} [index]\n * @param {array} [array]\n * @param {object} [thisArg]\n * @return {boolean}\n */\n\n\nexport default class PluginService {\n constructor() {\n this._init = [];\n }\n\n /**\n\t * Calls enabled plugins for `chart` on the specified hook and with the given args.\n\t * This method immediately returns as soon as a plugin explicitly returns false. The\n\t * returned value can be used, for instance, to interrupt the current action.\n\t * @param {Chart} chart - The chart instance for which plugins should be called.\n\t * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate').\n\t * @param {object} [args] - Extra arguments to apply to the hook call.\n * @param {filterCallback} [filter] - Filtering function for limiting which plugins are notified\n\t * @returns {boolean} false if any of the plugins return false, else returns true.\n\t */\n notify(chart, hook, args, filter) {\n if (hook === 'beforeInit') {\n this._init = this._createDescriptors(chart, true);\n this._notify(this._init, chart, 'install');\n }\n\n const descriptors = filter ? this._descriptors(chart).filter(filter) : this._descriptors(chart);\n const result = this._notify(descriptors, chart, hook, args);\n\n if (hook === 'afterDestroy') {\n this._notify(descriptors, chart, 'stop');\n this._notify(this._init, chart, 'uninstall');\n }\n return result;\n }\n\n /**\n\t * @private\n\t */\n _notify(descriptors, chart, hook, args) {\n args = args || {};\n for (const descriptor of descriptors) {\n const plugin = descriptor.plugin;\n const method = plugin[hook];\n const params = [chart, args, descriptor.options];\n if (callCallback(method, params, plugin) === false && args.cancelable) {\n return false;\n }\n }\n\n return true;\n }\n\n invalidate() {\n // When plugins are registered, there is the possibility of a double\n // invalidate situation. In this case, we only want to invalidate once.\n // If we invalidate multiple times, the `_oldCache` is lost and all of the\n // plugins are restarted without being correctly stopped.\n // See https://github.com/chartjs/Chart.js/issues/8147\n if (!isNullOrUndef(this._cache)) {\n this._oldCache = this._cache;\n this._cache = undefined;\n }\n }\n\n /**\n\t * @param {Chart} chart\n\t * @private\n\t */\n _descriptors(chart) {\n if (this._cache) {\n return this._cache;\n }\n\n const descriptors = this._cache = this._createDescriptors(chart);\n\n this._notifyStateChanges(chart);\n\n return descriptors;\n }\n\n _createDescriptors(chart, all) {\n const config = chart && chart.config;\n const options = valueOrDefault(config.options && config.options.plugins, {});\n const plugins = allPlugins(config);\n // options === false => all plugins are disabled\n return options === false && !all ? [] : createDescriptors(chart, plugins, options, all);\n }\n\n /**\n\t * @param {Chart} chart\n\t * @private\n\t */\n _notifyStateChanges(chart) {\n const previousDescriptors = this._oldCache || [];\n const descriptors = this._cache;\n const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id));\n this._notify(diff(previousDescriptors, descriptors), chart, 'stop');\n this._notify(diff(descriptors, previousDescriptors), chart, 'start');\n }\n}\n\n/**\n * @param {import('./core.config.js').default} config\n */\nfunction allPlugins(config) {\n const localIds = {};\n const plugins = [];\n const keys = Object.keys(registry.plugins.items);\n for (let i = 0; i < keys.length; i++) {\n plugins.push(registry.getPlugin(keys[i]));\n }\n\n const local = config.plugins || [];\n for (let i = 0; i < local.length; i++) {\n const plugin = local[i];\n\n if (plugins.indexOf(plugin) === -1) {\n plugins.push(plugin);\n localIds[plugin.id] = true;\n }\n }\n\n return {plugins, localIds};\n}\n\nfunction getOpts(options, all) {\n if (!all && options === false) {\n return null;\n }\n if (options === true) {\n return {};\n }\n return options;\n}\n\nfunction createDescriptors(chart, {plugins, localIds}, options, all) {\n const result = [];\n const context = chart.getContext();\n\n for (const plugin of plugins) {\n const id = plugin.id;\n const opts = getOpts(options[id], all);\n if (opts === null) {\n continue;\n }\n result.push({\n plugin,\n options: pluginOpts(chart.config, {plugin, local: localIds[id]}, opts, context)\n });\n }\n\n return result;\n}\n\nfunction pluginOpts(config, {plugin, local}, opts, context) {\n const keys = config.pluginScopeKeys(plugin);\n const scopes = config.getOptionScopes(opts, keys);\n if (local && plugin.defaults) {\n // make sure plugin defaults are in scopes for local (not registered) plugins\n scopes.push(plugin.defaults);\n }\n return config.createResolver(scopes, context, [''], {\n // These are just defaults that plugins can override\n scriptable: false,\n indexable: false,\n allKeys: true\n });\n}\n","import defaults, {overrides, descriptors} from './core.defaults.js';\nimport {mergeIf, resolveObjectKey, isArray, isFunction, valueOrDefault, isObject} from '../helpers/helpers.core.js';\nimport {_attachContext, _createResolver, _descriptors} from '../helpers/helpers.config.js';\n\nexport function getIndexAxis(type, options) {\n const datasetDefaults = defaults.datasets[type] || {};\n const datasetOptions = (options.datasets || {})[type] || {};\n return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x';\n}\n\nfunction getAxisFromDefaultScaleID(id, indexAxis) {\n let axis = id;\n if (id === '_index_') {\n axis = indexAxis;\n } else if (id === '_value_') {\n axis = indexAxis === 'x' ? 'y' : 'x';\n }\n return axis;\n}\n\nfunction getDefaultScaleIDFromAxis(axis, indexAxis) {\n return axis === indexAxis ? '_index_' : '_value_';\n}\n\nfunction idMatchesAxis(id) {\n if (id === 'x' || id === 'y' || id === 'r') {\n return id;\n }\n}\n\nfunction axisFromPosition(position) {\n if (position === 'top' || position === 'bottom') {\n return 'x';\n }\n if (position === 'left' || position === 'right') {\n return 'y';\n }\n}\n\nexport function determineAxis(id, ...scaleOptions) {\n if (idMatchesAxis(id)) {\n return id;\n }\n for (const opts of scaleOptions) {\n const axis = opts.axis\n || axisFromPosition(opts.position)\n || id.length > 1 && idMatchesAxis(id[0].toLowerCase());\n if (axis) {\n return axis;\n }\n }\n throw new Error(`Cannot determine type of '${id}' axis. Please provide 'axis' or 'position' option.`);\n}\n\nfunction getAxisFromDataset(id, axis, dataset) {\n if (dataset[axis + 'AxisID'] === id) {\n return {axis};\n }\n}\n\nfunction retrieveAxisFromDatasets(id, config) {\n if (config.data && config.data.datasets) {\n const boundDs = config.data.datasets.filter((d) => d.xAxisID === id || d.yAxisID === id);\n if (boundDs.length) {\n return getAxisFromDataset(id, 'x', boundDs[0]) || getAxisFromDataset(id, 'y', boundDs[0]);\n }\n }\n return {};\n}\n\nfunction mergeScaleConfig(config, options) {\n const chartDefaults = overrides[config.type] || {scales: {}};\n const configScales = options.scales || {};\n const chartIndexAxis = getIndexAxis(config.type, options);\n const scales = Object.create(null);\n\n // First figure out first scale id's per axis.\n Object.keys(configScales).forEach(id => {\n const scaleConf = configScales[id];\n if (!isObject(scaleConf)) {\n return console.error(`Invalid scale configuration for scale: ${id}`);\n }\n if (scaleConf._proxy) {\n return console.warn(`Ignoring resolver passed as options for scale: ${id}`);\n }\n const axis = determineAxis(id, scaleConf, retrieveAxisFromDatasets(id, config), defaults.scales[scaleConf.type]);\n const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis);\n const defaultScaleOptions = chartDefaults.scales || {};\n scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]);\n });\n\n // Then merge dataset defaults to scale configs\n config.data.datasets.forEach(dataset => {\n const type = dataset.type || config.type;\n const indexAxis = dataset.indexAxis || getIndexAxis(type, options);\n const datasetDefaults = overrides[type] || {};\n const defaultScaleOptions = datasetDefaults.scales || {};\n Object.keys(defaultScaleOptions).forEach(defaultID => {\n const axis = getAxisFromDefaultScaleID(defaultID, indexAxis);\n const id = dataset[axis + 'AxisID'] || axis;\n scales[id] = scales[id] || Object.create(null);\n mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]);\n });\n });\n\n // apply scale defaults, if not overridden by dataset defaults\n Object.keys(scales).forEach(key => {\n const scale = scales[key];\n mergeIf(scale, [defaults.scales[scale.type], defaults.scale]);\n });\n\n return scales;\n}\n\nfunction initOptions(config) {\n const options = config.options || (config.options = {});\n\n options.plugins = valueOrDefault(options.plugins, {});\n options.scales = mergeScaleConfig(config, options);\n}\n\nfunction initData(data) {\n data = data || {};\n data.datasets = data.datasets || [];\n data.labels = data.labels || [];\n return data;\n}\n\nfunction initConfig(config) {\n config = config || {};\n config.data = initData(config.data);\n\n initOptions(config);\n\n return config;\n}\n\nconst keyCache = new Map();\nconst keysCached = new Set();\n\nfunction cachedKeys(cacheKey, generate) {\n let keys = keyCache.get(cacheKey);\n if (!keys) {\n keys = generate();\n keyCache.set(cacheKey, keys);\n keysCached.add(keys);\n }\n return keys;\n}\n\nconst addIfFound = (set, obj, key) => {\n const opts = resolveObjectKey(obj, key);\n if (opts !== undefined) {\n set.add(opts);\n }\n};\n\nexport default class Config {\n constructor(config) {\n this._config = initConfig(config);\n this._scopeCache = new Map();\n this._resolverCache = new Map();\n }\n\n get platform() {\n return this._config.platform;\n }\n\n get type() {\n return this._config.type;\n }\n\n set type(type) {\n this._config.type = type;\n }\n\n get data() {\n return this._config.data;\n }\n\n set data(data) {\n this._config.data = initData(data);\n }\n\n get options() {\n return this._config.options;\n }\n\n set options(options) {\n this._config.options = options;\n }\n\n get plugins() {\n return this._config.plugins;\n }\n\n update() {\n const config = this._config;\n this.clearCache();\n initOptions(config);\n }\n\n clearCache() {\n this._scopeCache.clear();\n this._resolverCache.clear();\n }\n\n /**\n * Returns the option scope keys for resolving dataset options.\n * These keys do not include the dataset itself, because it is not under options.\n * @param {string} datasetType\n * @return {string[][]}\n */\n datasetScopeKeys(datasetType) {\n return cachedKeys(datasetType,\n () => [[\n `datasets.${datasetType}`,\n ''\n ]]);\n }\n\n /**\n * Returns the option scope keys for resolving dataset animation options.\n * These keys do not include the dataset itself, because it is not under options.\n * @param {string} datasetType\n * @param {string} transition\n * @return {string[][]}\n */\n datasetAnimationScopeKeys(datasetType, transition) {\n return cachedKeys(`${datasetType}.transition.${transition}`,\n () => [\n [\n `datasets.${datasetType}.transitions.${transition}`,\n `transitions.${transition}`,\n ],\n // The following are used for looking up the `animations` and `animation` keys\n [\n `datasets.${datasetType}`,\n ''\n ]\n ]);\n }\n\n /**\n * Returns the options scope keys for resolving element options that belong\n * to an dataset. These keys do not include the dataset itself, because it\n * is not under options.\n * @param {string} datasetType\n * @param {string} elementType\n * @return {string[][]}\n */\n datasetElementScopeKeys(datasetType, elementType) {\n return cachedKeys(`${datasetType}-${elementType}`,\n () => [[\n `datasets.${datasetType}.elements.${elementType}`,\n `datasets.${datasetType}`,\n `elements.${elementType}`,\n ''\n ]]);\n }\n\n /**\n * Returns the options scope keys for resolving plugin options.\n * @param {{id: string, additionalOptionScopes?: string[]}} plugin\n * @return {string[][]}\n */\n pluginScopeKeys(plugin) {\n const id = plugin.id;\n const type = this.type;\n return cachedKeys(`${type}-plugin-${id}`,\n () => [[\n `plugins.${id}`,\n ...plugin.additionalOptionScopes || [],\n ]]);\n }\n\n /**\n * @private\n */\n _cachedScopes(mainScope, resetCache) {\n const _scopeCache = this._scopeCache;\n let cache = _scopeCache.get(mainScope);\n if (!cache || resetCache) {\n cache = new Map();\n _scopeCache.set(mainScope, cache);\n }\n return cache;\n }\n\n /**\n * Resolves the objects from options and defaults for option value resolution.\n * @param {object} mainScope - The main scope object for options\n * @param {string[][]} keyLists - The arrays of keys in resolution order\n * @param {boolean} [resetCache] - reset the cache for this mainScope\n */\n getOptionScopes(mainScope, keyLists, resetCache) {\n const {options, type} = this;\n const cache = this._cachedScopes(mainScope, resetCache);\n const cached = cache.get(keyLists);\n if (cached) {\n return cached;\n }\n\n const scopes = new Set();\n\n keyLists.forEach(keys => {\n if (mainScope) {\n scopes.add(mainScope);\n keys.forEach(key => addIfFound(scopes, mainScope, key));\n }\n keys.forEach(key => addIfFound(scopes, options, key));\n keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key));\n keys.forEach(key => addIfFound(scopes, defaults, key));\n keys.forEach(key => addIfFound(scopes, descriptors, key));\n });\n\n const array = Array.from(scopes);\n if (array.length === 0) {\n array.push(Object.create(null));\n }\n if (keysCached.has(keyLists)) {\n cache.set(keyLists, array);\n }\n return array;\n }\n\n /**\n * Returns the option scopes for resolving chart options\n * @return {object[]}\n */\n chartOptionScopes() {\n const {options, type} = this;\n\n return [\n options,\n overrides[type] || {},\n defaults.datasets[type] || {}, // https://github.com/chartjs/Chart.js/issues/8531\n {type},\n defaults,\n descriptors\n ];\n }\n\n /**\n * @param {object[]} scopes\n * @param {string[]} names\n * @param {function|object} context\n * @param {string[]} [prefixes]\n * @return {object}\n */\n resolveNamedOptions(scopes, names, context, prefixes = ['']) {\n const result = {$shared: true};\n const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes);\n let options = resolver;\n if (needContext(resolver, names)) {\n result.$shared = false;\n context = isFunction(context) ? context() : context;\n // subResolver is passed to scriptable options. It should not resolve to hover options.\n const subResolver = this.createResolver(scopes, context, subPrefixes);\n options = _attachContext(resolver, context, subResolver);\n }\n\n for (const prop of names) {\n result[prop] = options[prop];\n }\n return result;\n }\n\n /**\n * @param {object[]} scopes\n * @param {object} [context]\n * @param {string[]} [prefixes]\n * @param {{scriptable: boolean, indexable: boolean, allKeys?: boolean}} [descriptorDefaults]\n */\n createResolver(scopes, context, prefixes = [''], descriptorDefaults) {\n const {resolver} = getResolver(this._resolverCache, scopes, prefixes);\n return isObject(context)\n ? _attachContext(resolver, context, undefined, descriptorDefaults)\n : resolver;\n }\n}\n\nfunction getResolver(resolverCache, scopes, prefixes) {\n let cache = resolverCache.get(scopes);\n if (!cache) {\n cache = new Map();\n resolverCache.set(scopes, cache);\n }\n const cacheKey = prefixes.join();\n let cached = cache.get(cacheKey);\n if (!cached) {\n const resolver = _createResolver(scopes, prefixes);\n cached = {\n resolver,\n subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover'))\n };\n cache.set(cacheKey, cached);\n }\n return cached;\n}\n\nconst hasFunction = value => isObject(value)\n && Object.getOwnPropertyNames(value).some((key) => isFunction(value[key]));\n\nfunction needContext(proxy, names) {\n const {isScriptable, isIndexable} = _descriptors(proxy);\n\n for (const prop of names) {\n const scriptable = isScriptable(prop);\n const indexable = isIndexable(prop);\n const value = (indexable || scriptable) && proxy[prop];\n if ((scriptable && (isFunction(value) || hasFunction(value)))\n || (indexable && isArray(value))) {\n return true;\n }\n }\n return false;\n}\n","import animator from './core.animator.js';\nimport defaults, {overrides} from './core.defaults.js';\nimport Interaction from './core.interaction.js';\nimport layouts from './core.layouts.js';\nimport {_detectPlatform} from '../platform/index.js';\nimport PluginService from './core.plugins.js';\nimport registry from './core.registry.js';\nimport Config, {determineAxis, getIndexAxis} from './core.config.js';\nimport {retinaScale, _isDomSupported} from '../helpers/helpers.dom.js';\nimport {each, callback as callCallback, uid, valueOrDefault, _elementsEqual, isNullOrUndef, setsEqual, defined, isFunction, _isClickEvent} from '../helpers/helpers.core.js';\nimport {clearCanvas, clipArea, createContext, unclipArea, _isPointInArea} from '../helpers/index.js';\n// @ts-ignore\nimport {version} from '../../package.json';\nimport {debounce} from '../helpers/helpers.extras.js';\n\n/**\n * @typedef { import('../types/index.js').ChartEvent } ChartEvent\n * @typedef { import('../types/index.js').Point } Point\n */\n\nconst KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea'];\nfunction positionIsHorizontal(position, axis) {\n return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x');\n}\n\nfunction compare2Level(l1, l2) {\n return function(a, b) {\n return a[l1] === b[l1]\n ? a[l2] - b[l2]\n : a[l1] - b[l1];\n };\n}\n\nfunction onAnimationsComplete(context) {\n const chart = context.chart;\n const animationOptions = chart.options.animation;\n\n chart.notifyPlugins('afterRender');\n callCallback(animationOptions && animationOptions.onComplete, [context], chart);\n}\n\nfunction onAnimationProgress(context) {\n const chart = context.chart;\n const animationOptions = chart.options.animation;\n callCallback(animationOptions && animationOptions.onProgress, [context], chart);\n}\n\n/**\n * Chart.js can take a string id of a canvas element, a 2d context, or a canvas element itself.\n * Attempt to unwrap the item passed into the chart constructor so that it is a canvas element (if possible).\n */\nfunction getCanvas(item) {\n if (_isDomSupported() && typeof item === 'string') {\n item = document.getElementById(item);\n } else if (item && item.length) {\n // Support for array based queries (such as jQuery)\n item = item[0];\n }\n\n if (item && item.canvas) {\n // Support for any object associated to a canvas (including a context2d)\n item = item.canvas;\n }\n return item;\n}\n\nconst instances = {};\nconst getChart = (key) => {\n const canvas = getCanvas(key);\n return Object.values(instances).filter((c) => c.canvas === canvas).pop();\n};\n\nfunction moveNumericKeys(obj, start, move) {\n const keys = Object.keys(obj);\n for (const key of keys) {\n const intKey = +key;\n if (intKey >= start) {\n const value = obj[key];\n delete obj[key];\n if (move > 0 || intKey > start) {\n obj[intKey + move] = value;\n }\n }\n }\n}\n\n/**\n * @param {ChartEvent} e\n * @param {ChartEvent|null} lastEvent\n * @param {boolean} inChartArea\n * @param {boolean} isClick\n * @returns {ChartEvent|null}\n */\nfunction determineLastEvent(e, lastEvent, inChartArea, isClick) {\n if (!inChartArea || e.type === 'mouseout') {\n return null;\n }\n if (isClick) {\n return lastEvent;\n }\n return e;\n}\n\nfunction getSizeForArea(scale, chartArea, field) {\n return scale.options.clip ? scale[field] : chartArea[field];\n}\n\nfunction getDatasetArea(meta, chartArea) {\n const {xScale, yScale} = meta;\n if (xScale && yScale) {\n return {\n left: getSizeForArea(xScale, chartArea, 'left'),\n right: getSizeForArea(xScale, chartArea, 'right'),\n top: getSizeForArea(yScale, chartArea, 'top'),\n bottom: getSizeForArea(yScale, chartArea, 'bottom')\n };\n }\n return chartArea;\n}\n\nclass Chart {\n\n static defaults = defaults;\n static instances = instances;\n static overrides = overrides;\n static registry = registry;\n static version = version;\n static getChart = getChart;\n\n static register(...items) {\n registry.add(...items);\n invalidatePlugins();\n }\n\n static unregister(...items) {\n registry.remove(...items);\n invalidatePlugins();\n }\n\n // eslint-disable-next-line max-statements\n constructor(item, userConfig) {\n const config = this.config = new Config(userConfig);\n const initialCanvas = getCanvas(item);\n const existingChart = getChart(initialCanvas);\n if (existingChart) {\n throw new Error(\n 'Canvas is already in use. Chart with ID \\'' + existingChart.id + '\\'' +\n\t\t\t\t' must be destroyed before the canvas with ID \\'' + existingChart.canvas.id + '\\' can be reused.'\n );\n }\n\n const options = config.createResolver(config.chartOptionScopes(), this.getContext());\n\n this.platform = new (config.platform || _detectPlatform(initialCanvas))();\n this.platform.updateConfig(config);\n\n const context = this.platform.acquireContext(initialCanvas, options.aspectRatio);\n const canvas = context && context.canvas;\n const height = canvas && canvas.height;\n const width = canvas && canvas.width;\n\n this.id = uid();\n this.ctx = context;\n this.canvas = canvas;\n this.width = width;\n this.height = height;\n this._options = options;\n // Store the previously used aspect ratio to determine if a resize\n // is needed during updates. Do this after _options is set since\n // aspectRatio uses a getter\n this._aspectRatio = this.aspectRatio;\n this._layers = [];\n this._metasets = [];\n this._stacks = undefined;\n this.boxes = [];\n this.currentDevicePixelRatio = undefined;\n this.chartArea = undefined;\n this._active = [];\n this._lastEvent = undefined;\n this._listeners = {};\n /** @type {?{attach?: function, detach?: function, resize?: function}} */\n this._responsiveListeners = undefined;\n this._sortedMetasets = [];\n this.scales = {};\n this._plugins = new PluginService();\n this.$proxies = {};\n this._hiddenIndices = {};\n this.attached = false;\n this._animationsDisabled = undefined;\n this.$context = undefined;\n this._doResize = debounce(mode => this.update(mode), options.resizeDelay || 0);\n this._dataChanges = [];\n\n // Add the chart instance to the global namespace\n instances[this.id] = this;\n\n if (!context || !canvas) {\n // The given item is not a compatible context2d element, let's return before finalizing\n // the chart initialization but after setting basic chart / controller properties that\n // can help to figure out that the chart is not valid (e.g chart.canvas !== null);\n // https://github.com/chartjs/Chart.js/issues/2807\n console.error(\"Failed to create chart: can't acquire context from the given item\");\n return;\n }\n\n animator.listen(this, 'complete', onAnimationsComplete);\n animator.listen(this, 'progress', onAnimationProgress);\n\n this._initialize();\n if (this.attached) {\n this.update();\n }\n }\n\n get aspectRatio() {\n const {options: {aspectRatio, maintainAspectRatio}, width, height, _aspectRatio} = this;\n if (!isNullOrUndef(aspectRatio)) {\n // If aspectRatio is defined in options, use that.\n return aspectRatio;\n }\n\n if (maintainAspectRatio && _aspectRatio) {\n // If maintainAspectRatio is truthly and we had previously determined _aspectRatio, use that\n return _aspectRatio;\n }\n\n // Calculate\n return height ? width / height : null;\n }\n\n get data() {\n return this.config.data;\n }\n\n set data(data) {\n this.config.data = data;\n }\n\n get options() {\n return this._options;\n }\n\n set options(options) {\n this.config.options = options;\n }\n\n get registry() {\n return registry;\n }\n\n /**\n\t * @private\n\t */\n _initialize() {\n // Before init plugin notification\n this.notifyPlugins('beforeInit');\n\n if (this.options.responsive) {\n this.resize();\n } else {\n retinaScale(this, this.options.devicePixelRatio);\n }\n\n this.bindEvents();\n\n // After init plugin notification\n this.notifyPlugins('afterInit');\n\n return this;\n }\n\n clear() {\n clearCanvas(this.canvas, this.ctx);\n return this;\n }\n\n stop() {\n animator.stop(this);\n return this;\n }\n\n /**\n\t * Resize the chart to its container or to explicit dimensions.\n\t * @param {number} [width]\n\t * @param {number} [height]\n\t */\n resize(width, height) {\n if (!animator.running(this)) {\n this._resize(width, height);\n } else {\n this._resizeBeforeDraw = {width, height};\n }\n }\n\n _resize(width, height) {\n const options = this.options;\n const canvas = this.canvas;\n const aspectRatio = options.maintainAspectRatio && this.aspectRatio;\n const newSize = this.platform.getMaximumSize(canvas, width, height, aspectRatio);\n const newRatio = options.devicePixelRatio || this.platform.getDevicePixelRatio();\n const mode = this.width ? 'resize' : 'attach';\n\n this.width = newSize.width;\n this.height = newSize.height;\n this._aspectRatio = this.aspectRatio;\n if (!retinaScale(this, newRatio, true)) {\n return;\n }\n\n this.notifyPlugins('resize', {size: newSize});\n\n callCallback(options.onResize, [this, newSize], this);\n\n if (this.attached) {\n if (this._doResize(mode)) {\n // The resize update is delayed, only draw without updating.\n this.render();\n }\n }\n }\n\n ensureScalesHaveIDs() {\n const options = this.options;\n const scalesOptions = options.scales || {};\n\n each(scalesOptions, (axisOptions, axisID) => {\n axisOptions.id = axisID;\n });\n }\n\n /**\n\t * Builds a map of scale ID to scale object for future lookup.\n\t */\n buildOrUpdateScales() {\n const options = this.options;\n const scaleOpts = options.scales;\n const scales = this.scales;\n const updated = Object.keys(scales).reduce((obj, id) => {\n obj[id] = false;\n return obj;\n }, {});\n let items = [];\n\n if (scaleOpts) {\n items = items.concat(\n Object.keys(scaleOpts).map((id) => {\n const scaleOptions = scaleOpts[id];\n const axis = determineAxis(id, scaleOptions);\n const isRadial = axis === 'r';\n const isHorizontal = axis === 'x';\n return {\n options: scaleOptions,\n dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left',\n dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear'\n };\n })\n );\n }\n\n each(items, (item) => {\n const scaleOptions = item.options;\n const id = scaleOptions.id;\n const axis = determineAxis(id, scaleOptions);\n const scaleType = valueOrDefault(scaleOptions.type, item.dtype);\n\n if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) {\n scaleOptions.position = item.dposition;\n }\n\n updated[id] = true;\n let scale = null;\n if (id in scales && scales[id].type === scaleType) {\n scale = scales[id];\n } else {\n const scaleClass = registry.getScale(scaleType);\n scale = new scaleClass({\n id,\n type: scaleType,\n ctx: this.ctx,\n chart: this\n });\n scales[scale.id] = scale;\n }\n\n scale.init(scaleOptions, options);\n });\n // clear up discarded scales\n each(updated, (hasUpdated, id) => {\n if (!hasUpdated) {\n delete scales[id];\n }\n });\n\n each(scales, (scale) => {\n layouts.configure(this, scale, scale.options);\n layouts.addBox(this, scale);\n });\n }\n\n /**\n\t * @private\n\t */\n _updateMetasets() {\n const metasets = this._metasets;\n const numData = this.data.datasets.length;\n const numMeta = metasets.length;\n\n metasets.sort((a, b) => a.index - b.index);\n if (numMeta > numData) {\n for (let i = numData; i < numMeta; ++i) {\n this._destroyDatasetMeta(i);\n }\n metasets.splice(numData, numMeta - numData);\n }\n this._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index'));\n }\n\n /**\n\t * @private\n\t */\n _removeUnreferencedMetasets() {\n const {_metasets: metasets, data: {datasets}} = this;\n if (metasets.length > datasets.length) {\n delete this._stacks;\n }\n metasets.forEach((meta, index) => {\n if (datasets.filter(x => x === meta._dataset).length === 0) {\n this._destroyDatasetMeta(index);\n }\n });\n }\n\n buildOrUpdateControllers() {\n const newControllers = [];\n const datasets = this.data.datasets;\n let i, ilen;\n\n this._removeUnreferencedMetasets();\n\n for (i = 0, ilen = datasets.length; i < ilen; i++) {\n const dataset = datasets[i];\n let meta = this.getDatasetMeta(i);\n const type = dataset.type || this.config.type;\n\n if (meta.type && meta.type !== type) {\n this._destroyDatasetMeta(i);\n meta = this.getDatasetMeta(i);\n }\n meta.type = type;\n meta.indexAxis = dataset.indexAxis || getIndexAxis(type, this.options);\n meta.order = dataset.order || 0;\n meta.index = i;\n meta.label = '' + dataset.label;\n meta.visible = this.isDatasetVisible(i);\n\n if (meta.controller) {\n meta.controller.updateIndex(i);\n meta.controller.linkScales();\n } else {\n const ControllerClass = registry.getController(type);\n const {datasetElementType, dataElementType} = defaults.datasets[type];\n Object.assign(ControllerClass, {\n dataElementType: registry.getElement(dataElementType),\n datasetElementType: datasetElementType && registry.getElement(datasetElementType)\n });\n meta.controller = new ControllerClass(this, i);\n newControllers.push(meta.controller);\n }\n }\n\n this._updateMetasets();\n return newControllers;\n }\n\n /**\n\t * Reset the elements of all datasets\n\t * @private\n\t */\n _resetElements() {\n each(this.data.datasets, (dataset, datasetIndex) => {\n this.getDatasetMeta(datasetIndex).controller.reset();\n }, this);\n }\n\n /**\n\t* Resets the chart back to its state before the initial animation\n\t*/\n reset() {\n this._resetElements();\n this.notifyPlugins('reset');\n }\n\n update(mode) {\n const config = this.config;\n\n config.update();\n const options = this._options = config.createResolver(config.chartOptionScopes(), this.getContext());\n const animsDisabled = this._animationsDisabled = !options.animation;\n\n this._updateScales();\n this._checkEventBindings();\n this._updateHiddenIndices();\n\n // plugins options references might have change, let's invalidate the cache\n // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167\n this._plugins.invalidate();\n\n if (this.notifyPlugins('beforeUpdate', {mode, cancelable: true}) === false) {\n return;\n }\n\n // Make sure dataset controllers are updated and new controllers are reset\n const newControllers = this.buildOrUpdateControllers();\n\n this.notifyPlugins('beforeElementsUpdate');\n\n // Make sure all dataset controllers have correct meta data counts\n let minPadding = 0;\n for (let i = 0, ilen = this.data.datasets.length; i < ilen; i++) {\n const {controller} = this.getDatasetMeta(i);\n const reset = !animsDisabled && newControllers.indexOf(controller) === -1;\n // New controllers will be reset after the layout pass, so we only want to modify\n // elements added to new datasets\n controller.buildOrUpdateElements(reset);\n minPadding = Math.max(+controller.getMaxOverflow(), minPadding);\n }\n minPadding = this._minPadding = options.layout.autoPadding ? minPadding : 0;\n this._updateLayout(minPadding);\n\n // Only reset the controllers if we have animations\n if (!animsDisabled) {\n // Can only reset the new controllers after the scales have been updated\n // Reset is done to get the starting point for the initial animation\n each(newControllers, (controller) => {\n controller.reset();\n });\n }\n\n this._updateDatasets(mode);\n\n // Do this before render so that any plugins that need final scale updates can use it\n this.notifyPlugins('afterUpdate', {mode});\n\n this._layers.sort(compare2Level('z', '_idx'));\n\n // Replay last event from before update, or set hover styles on active elements\n const {_active, _lastEvent} = this;\n if (_lastEvent) {\n this._eventHandler(_lastEvent, true);\n } else if (_active.length) {\n this._updateHoverStyles(_active, _active, true);\n }\n\n this.render();\n }\n\n /**\n * @private\n */\n _updateScales() {\n each(this.scales, (scale) => {\n layouts.removeBox(this, scale);\n });\n\n this.ensureScalesHaveIDs();\n this.buildOrUpdateScales();\n }\n\n /**\n * @private\n */\n _checkEventBindings() {\n const options = this.options;\n const existingEvents = new Set(Object.keys(this._listeners));\n const newEvents = new Set(options.events);\n\n if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== options.responsive) {\n // The configured events have changed. Rebind.\n this.unbindEvents();\n this.bindEvents();\n }\n }\n\n /**\n * @private\n */\n _updateHiddenIndices() {\n const {_hiddenIndices} = this;\n const changes = this._getUniformDataChanges() || [];\n for (const {method, start, count} of changes) {\n const move = method === '_removeElements' ? -count : count;\n moveNumericKeys(_hiddenIndices, start, move);\n }\n }\n\n /**\n * @private\n */\n _getUniformDataChanges() {\n const _dataChanges = this._dataChanges;\n if (!_dataChanges || !_dataChanges.length) {\n return;\n }\n\n this._dataChanges = [];\n const datasetCount = this.data.datasets.length;\n const makeSet = (idx) => new Set(\n _dataChanges\n .filter(c => c[0] === idx)\n .map((c, i) => i + ',' + c.splice(1).join(','))\n );\n\n const changeSet = makeSet(0);\n for (let i = 1; i < datasetCount; i++) {\n if (!setsEqual(changeSet, makeSet(i))) {\n return;\n }\n }\n return Array.from(changeSet)\n .map(c => c.split(','))\n .map(a => ({method: a[1], start: +a[2], count: +a[3]}));\n }\n\n /**\n\t * Updates the chart layout unless a plugin returns `false` to the `beforeLayout`\n\t * hook, in which case, plugins will not be called on `afterLayout`.\n\t * @private\n\t */\n _updateLayout(minPadding) {\n if (this.notifyPlugins('beforeLayout', {cancelable: true}) === false) {\n return;\n }\n\n layouts.update(this, this.width, this.height, minPadding);\n\n const area = this.chartArea;\n const noArea = area.width <= 0 || area.height <= 0;\n\n this._layers = [];\n each(this.boxes, (box) => {\n if (noArea && box.position === 'chartArea') {\n // Skip drawing and configuring chartArea boxes when chartArea is zero or negative\n return;\n }\n\n // configure is called twice, once in core.scale.update and once here.\n // Here the boxes are fully updated and at their final positions.\n if (box.configure) {\n box.configure();\n }\n this._layers.push(...box._layers());\n }, this);\n\n this._layers.forEach((item, index) => {\n item._idx = index;\n });\n\n this.notifyPlugins('afterLayout');\n }\n\n /**\n\t * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate`\n\t * hook, in which case, plugins will not be called on `afterDatasetsUpdate`.\n\t * @private\n\t */\n _updateDatasets(mode) {\n if (this.notifyPlugins('beforeDatasetsUpdate', {mode, cancelable: true}) === false) {\n return;\n }\n\n for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) {\n this.getDatasetMeta(i).controller.configure();\n }\n\n for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) {\n this._updateDataset(i, isFunction(mode) ? mode({datasetIndex: i}) : mode);\n }\n\n this.notifyPlugins('afterDatasetsUpdate', {mode});\n }\n\n /**\n\t * Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate`\n\t * hook, in which case, plugins will not be called on `afterDatasetUpdate`.\n\t * @private\n\t */\n _updateDataset(index, mode) {\n const meta = this.getDatasetMeta(index);\n const args = {meta, index, mode, cancelable: true};\n\n if (this.notifyPlugins('beforeDatasetUpdate', args) === false) {\n return;\n }\n\n meta.controller._update(mode);\n\n args.cancelable = false;\n this.notifyPlugins('afterDatasetUpdate', args);\n }\n\n render() {\n if (this.notifyPlugins('beforeRender', {cancelable: true}) === false) {\n return;\n }\n\n if (animator.has(this)) {\n if (this.attached && !animator.running(this)) {\n animator.start(this);\n }\n } else {\n this.draw();\n onAnimationsComplete({chart: this});\n }\n }\n\n draw() {\n let i;\n if (this._resizeBeforeDraw) {\n const {width, height} = this._resizeBeforeDraw;\n // Unset pending resize request now to avoid possible recursion within _resize\n this._resizeBeforeDraw = null;\n this._resize(width, height);\n }\n this.clear();\n\n if (this.width <= 0 || this.height <= 0) {\n return;\n }\n\n if (this.notifyPlugins('beforeDraw', {cancelable: true}) === false) {\n return;\n }\n\n // Because of plugin hooks (before/afterDatasetsDraw), datasets can't\n // currently be part of layers. Instead, we draw\n // layers <= 0 before(default, backward compat), and the rest after\n const layers = this._layers;\n for (i = 0; i < layers.length && layers[i].z <= 0; ++i) {\n layers[i].draw(this.chartArea);\n }\n\n this._drawDatasets();\n\n // Rest of layers\n for (; i < layers.length; ++i) {\n layers[i].draw(this.chartArea);\n }\n\n this.notifyPlugins('afterDraw');\n }\n\n /**\n\t * @private\n\t */\n _getSortedDatasetMetas(filterVisible) {\n const metasets = this._sortedMetasets;\n const result = [];\n let i, ilen;\n\n for (i = 0, ilen = metasets.length; i < ilen; ++i) {\n const meta = metasets[i];\n if (!filterVisible || meta.visible) {\n result.push(meta);\n }\n }\n\n return result;\n }\n\n /**\n\t * Gets the visible dataset metas in drawing order\n\t * @return {object[]}\n\t */\n getSortedVisibleDatasetMetas() {\n return this._getSortedDatasetMetas(true);\n }\n\n /**\n\t * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw`\n\t * hook, in which case, plugins will not be called on `afterDatasetsDraw`.\n\t * @private\n\t */\n _drawDatasets() {\n if (this.notifyPlugins('beforeDatasetsDraw', {cancelable: true}) === false) {\n return;\n }\n\n const metasets = this.getSortedVisibleDatasetMetas();\n for (let i = metasets.length - 1; i >= 0; --i) {\n this._drawDataset(metasets[i]);\n }\n\n this.notifyPlugins('afterDatasetsDraw');\n }\n\n /**\n\t * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw`\n\t * hook, in which case, plugins will not be called on `afterDatasetDraw`.\n\t * @private\n\t */\n _drawDataset(meta) {\n const ctx = this.ctx;\n const clip = meta._clip;\n const useClip = !clip.disabled;\n const area = getDatasetArea(meta, this.chartArea);\n const args = {\n meta,\n index: meta.index,\n cancelable: true\n };\n\n if (this.notifyPlugins('beforeDatasetDraw', args) === false) {\n return;\n }\n\n if (useClip) {\n clipArea(ctx, {\n left: clip.left === false ? 0 : area.left - clip.left,\n right: clip.right === false ? this.width : area.right + clip.right,\n top: clip.top === false ? 0 : area.top - clip.top,\n bottom: clip.bottom === false ? this.height : area.bottom + clip.bottom\n });\n }\n\n meta.controller.draw();\n\n if (useClip) {\n unclipArea(ctx);\n }\n\n args.cancelable = false;\n this.notifyPlugins('afterDatasetDraw', args);\n }\n\n /**\n * Checks whether the given point is in the chart area.\n * @param {Point} point - in relative coordinates (see, e.g., getRelativePosition)\n * @returns {boolean}\n */\n isPointInArea(point) {\n return _isPointInArea(point, this.chartArea, this._minPadding);\n }\n\n getElementsAtEventForMode(e, mode, options, useFinalPosition) {\n const method = Interaction.modes[mode];\n if (typeof method === 'function') {\n return method(this, e, options, useFinalPosition);\n }\n\n return [];\n }\n\n getDatasetMeta(datasetIndex) {\n const dataset = this.data.datasets[datasetIndex];\n const metasets = this._metasets;\n let meta = metasets.filter(x => x && x._dataset === dataset).pop();\n\n if (!meta) {\n meta = {\n type: null,\n data: [],\n dataset: null,\n controller: null,\n hidden: null,\t\t\t// See isDatasetVisible() comment\n xAxisID: null,\n yAxisID: null,\n order: dataset && dataset.order || 0,\n index: datasetIndex,\n _dataset: dataset,\n _parsed: [],\n _sorted: false\n };\n metasets.push(meta);\n }\n\n return meta;\n }\n\n getContext() {\n return this.$context || (this.$context = createContext(null, {chart: this, type: 'chart'}));\n }\n\n getVisibleDatasetCount() {\n return this.getSortedVisibleDatasetMetas().length;\n }\n\n isDatasetVisible(datasetIndex) {\n const dataset = this.data.datasets[datasetIndex];\n if (!dataset) {\n return false;\n }\n\n const meta = this.getDatasetMeta(datasetIndex);\n\n // meta.hidden is a per chart dataset hidden flag override with 3 states: if true or false,\n // the dataset.hidden value is ignored, else if null, the dataset hidden state is returned.\n return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden;\n }\n\n setDatasetVisibility(datasetIndex, visible) {\n const meta = this.getDatasetMeta(datasetIndex);\n meta.hidden = !visible;\n }\n\n toggleDataVisibility(index) {\n this._hiddenIndices[index] = !this._hiddenIndices[index];\n }\n\n getDataVisibility(index) {\n return !this._hiddenIndices[index];\n }\n\n /**\n\t * @private\n\t */\n _updateVisibility(datasetIndex, dataIndex, visible) {\n const mode = visible ? 'show' : 'hide';\n const meta = this.getDatasetMeta(datasetIndex);\n const anims = meta.controller._resolveAnimations(undefined, mode);\n\n if (defined(dataIndex)) {\n meta.data[dataIndex].hidden = !visible;\n this.update();\n } else {\n this.setDatasetVisibility(datasetIndex, visible);\n // Animate visible state, so hide animation can be seen. This could be handled better if update / updateDataset returned a Promise.\n anims.update(meta, {visible});\n this.update((ctx) => ctx.datasetIndex === datasetIndex ? mode : undefined);\n }\n }\n\n hide(datasetIndex, dataIndex) {\n this._updateVisibility(datasetIndex, dataIndex, false);\n }\n\n show(datasetIndex, dataIndex) {\n this._updateVisibility(datasetIndex, dataIndex, true);\n }\n\n /**\n\t * @private\n\t */\n _destroyDatasetMeta(datasetIndex) {\n const meta = this._metasets[datasetIndex];\n if (meta && meta.controller) {\n meta.controller._destroy();\n }\n delete this._metasets[datasetIndex];\n }\n\n _stop() {\n let i, ilen;\n this.stop();\n animator.remove(this);\n\n for (i = 0, ilen = this.data.datasets.length; i < ilen; ++i) {\n this._destroyDatasetMeta(i);\n }\n }\n\n destroy() {\n this.notifyPlugins('beforeDestroy');\n const {canvas, ctx} = this;\n\n this._stop();\n this.config.clearCache();\n\n if (canvas) {\n this.unbindEvents();\n clearCanvas(canvas, ctx);\n this.platform.releaseContext(ctx);\n this.canvas = null;\n this.ctx = null;\n }\n\n delete instances[this.id];\n\n this.notifyPlugins('afterDestroy');\n }\n\n toBase64Image(...args) {\n return this.canvas.toDataURL(...args);\n }\n\n /**\n\t * @private\n\t */\n bindEvents() {\n this.bindUserEvents();\n if (this.options.responsive) {\n this.bindResponsiveEvents();\n } else {\n this.attached = true;\n }\n }\n\n /**\n * @private\n */\n bindUserEvents() {\n const listeners = this._listeners;\n const platform = this.platform;\n\n const _add = (type, listener) => {\n platform.addEventListener(this, type, listener);\n listeners[type] = listener;\n };\n\n const listener = (e, x, y) => {\n e.offsetX = x;\n e.offsetY = y;\n this._eventHandler(e);\n };\n\n each(this.options.events, (type) => _add(type, listener));\n }\n\n /**\n * @private\n */\n bindResponsiveEvents() {\n if (!this._responsiveListeners) {\n this._responsiveListeners = {};\n }\n const listeners = this._responsiveListeners;\n const platform = this.platform;\n\n const _add = (type, listener) => {\n platform.addEventListener(this, type, listener);\n listeners[type] = listener;\n };\n const _remove = (type, listener) => {\n if (listeners[type]) {\n platform.removeEventListener(this, type, listener);\n delete listeners[type];\n }\n };\n\n const listener = (width, height) => {\n if (this.canvas) {\n this.resize(width, height);\n }\n };\n\n let detached; // eslint-disable-line prefer-const\n const attached = () => {\n _remove('attach', attached);\n\n this.attached = true;\n this.resize();\n\n _add('resize', listener);\n _add('detach', detached);\n };\n\n detached = () => {\n this.attached = false;\n\n _remove('resize', listener);\n\n // Stop animating and remove metasets, so when re-attached, the animations start from beginning.\n this._stop();\n this._resize(0, 0);\n\n _add('attach', attached);\n };\n\n if (platform.isAttached(this.canvas)) {\n attached();\n } else {\n detached();\n }\n }\n\n /**\n\t * @private\n\t */\n unbindEvents() {\n each(this._listeners, (listener, type) => {\n this.platform.removeEventListener(this, type, listener);\n });\n this._listeners = {};\n\n each(this._responsiveListeners, (listener, type) => {\n this.platform.removeEventListener(this, type, listener);\n });\n this._responsiveListeners = undefined;\n }\n\n updateHoverStyle(items, mode, enabled) {\n const prefix = enabled ? 'set' : 'remove';\n let meta, item, i, ilen;\n\n if (mode === 'dataset') {\n meta = this.getDatasetMeta(items[0].datasetIndex);\n meta.controller['_' + prefix + 'DatasetHoverStyle']();\n }\n\n for (i = 0, ilen = items.length; i < ilen; ++i) {\n item = items[i];\n const controller = item && this.getDatasetMeta(item.datasetIndex).controller;\n if (controller) {\n controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index);\n }\n }\n }\n\n /**\n\t * Get active (hovered) elements\n\t * @returns array\n\t */\n getActiveElements() {\n return this._active || [];\n }\n\n /**\n\t * Set active (hovered) elements\n\t * @param {array} activeElements New active data points\n\t */\n setActiveElements(activeElements) {\n const lastActive = this._active || [];\n const active = activeElements.map(({datasetIndex, index}) => {\n const meta = this.getDatasetMeta(datasetIndex);\n if (!meta) {\n throw new Error('No dataset found at index ' + datasetIndex);\n }\n\n return {\n datasetIndex,\n element: meta.data[index],\n index,\n };\n });\n const changed = !_elementsEqual(active, lastActive);\n\n if (changed) {\n this._active = active;\n // Make sure we don't use the previous mouse event to override the active elements in update.\n this._lastEvent = null;\n this._updateHoverStyles(active, lastActive);\n }\n }\n\n /**\n\t * Calls enabled plugins on the specified hook and with the given args.\n\t * This method immediately returns as soon as a plugin explicitly returns false. The\n\t * returned value can be used, for instance, to interrupt the current action.\n\t * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate').\n\t * @param {Object} [args] - Extra arguments to apply to the hook call.\n * @param {import('./core.plugins.js').filterCallback} [filter] - Filtering function for limiting which plugins are notified\n\t * @returns {boolean} false if any of the plugins return false, else returns true.\n\t */\n notifyPlugins(hook, args, filter) {\n return this._plugins.notify(this, hook, args, filter);\n }\n\n /**\n * Check if a plugin with the specific ID is registered and enabled\n * @param {string} pluginId - The ID of the plugin of which to check if it is enabled\n * @returns {boolean}\n */\n isPluginEnabled(pluginId) {\n return this._plugins._cache.filter(p => p.plugin.id === pluginId).length === 1;\n }\n\n /**\n\t * @private\n\t */\n _updateHoverStyles(active, lastActive, replay) {\n const hoverOptions = this.options.hover;\n const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index));\n const deactivated = diff(lastActive, active);\n const activated = replay ? active : diff(active, lastActive);\n\n if (deactivated.length) {\n this.updateHoverStyle(deactivated, hoverOptions.mode, false);\n }\n\n if (activated.length && hoverOptions.mode) {\n this.updateHoverStyle(activated, hoverOptions.mode, true);\n }\n }\n\n /**\n\t * @private\n\t */\n _eventHandler(e, replay) {\n const args = {\n event: e,\n replay,\n cancelable: true,\n inChartArea: this.isPointInArea(e)\n };\n const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.native.type);\n\n if (this.notifyPlugins('beforeEvent', args, eventFilter) === false) {\n return;\n }\n\n const changed = this._handleEvent(e, replay, args.inChartArea);\n\n args.cancelable = false;\n this.notifyPlugins('afterEvent', args, eventFilter);\n\n if (changed || args.changed) {\n this.render();\n }\n\n return this;\n }\n\n /**\n\t * Handle an event\n\t * @param {ChartEvent} e the event to handle\n\t * @param {boolean} [replay] - true if the event was replayed by `update`\n * @param {boolean} [inChartArea] - true if the event is inside chartArea\n\t * @return {boolean} true if the chart needs to re-render\n\t * @private\n\t */\n _handleEvent(e, replay, inChartArea) {\n const {_active: lastActive = [], options} = this;\n\n // If the event is replayed from `update`, we should evaluate with the final positions.\n //\n // The `replay`:\n // It's the last event (excluding click) that has occurred before `update`.\n // So mouse has not moved. It's also over the chart, because there is a `replay`.\n //\n // The why:\n // If animations are active, the elements haven't moved yet compared to state before update.\n // But if they will, we are activating the elements that would be active, if this check\n // was done after the animations have completed. => \"final positions\".\n // If there is no animations, the \"final\" and \"current\" positions are equal.\n // This is done so we do not have to evaluate the active elements each animation frame\n // - it would be expensive.\n const useFinalPosition = replay;\n const active = this._getActiveElements(e, lastActive, inChartArea, useFinalPosition);\n const isClick = _isClickEvent(e);\n const lastEvent = determineLastEvent(e, this._lastEvent, inChartArea, isClick);\n\n if (inChartArea) {\n // Set _lastEvent to null while we are processing the event handlers.\n // This prevents recursion if the handler calls chart.update()\n this._lastEvent = null;\n\n // Invoke onHover hook\n callCallback(options.onHover, [e, active, this], this);\n\n if (isClick) {\n callCallback(options.onClick, [e, active, this], this);\n }\n }\n\n const changed = !_elementsEqual(active, lastActive);\n if (changed || replay) {\n this._active = active;\n this._updateHoverStyles(active, lastActive, replay);\n }\n\n this._lastEvent = lastEvent;\n\n return changed;\n }\n\n /**\n * @param {ChartEvent} e - The event\n * @param {import('../types/index.js').ActiveElement[]} lastActive - Previously active elements\n * @param {boolean} inChartArea - Is the event inside chartArea\n * @param {boolean} useFinalPosition - Should the evaluation be done with current or final (after animation) element positions\n * @returns {import('../types/index.js').ActiveElement[]} - The active elements\n * @pravate\n */\n _getActiveElements(e, lastActive, inChartArea, useFinalPosition) {\n if (e.type === 'mouseout') {\n return [];\n }\n\n if (!inChartArea) {\n // Let user control the active elements outside chartArea. Eg. using Legend.\n return lastActive;\n }\n\n const hoverOptions = this.options.hover;\n return this.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition);\n }\n}\n\n// @ts-ignore\nfunction invalidatePlugins() {\n return each(Chart.instances, (chart) => chart._plugins.invalidate());\n}\n\nexport default Chart;\n","/**\n * @namespace Chart._adapters\n * @since 2.8.0\n * @private\n */\n\nimport type {AnyObject} from '../types/basic.js';\nimport type {ChartOptions} from '../types/index.js';\n\nexport type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';\n\nexport interface DateAdapter {\n readonly options: T;\n /**\n * Will called with chart options after adapter creation.\n */\n init(this: DateAdapter, chartOptions: ChartOptions): void;\n /**\n * Returns a map of time formats for the supported formatting units defined\n * in Unit as well as 'datetime' representing a detailed date/time string.\n */\n formats(this: DateAdapter): Record;\n /**\n * Parses the given `value` and return the associated timestamp.\n * @param value - the value to parse (usually comes from the data)\n * @param [format] - the expected data format\n */\n parse(this: DateAdapter, value: unknown, format?: TimeUnit): number | null;\n /**\n * Returns the formatted date in the specified `format` for a given `timestamp`.\n * @param timestamp - the timestamp to format\n * @param format - the date/time token\n */\n format(this: DateAdapter, timestamp: number, format: TimeUnit): string;\n /**\n * Adds the specified `amount` of `unit` to the given `timestamp`.\n * @param timestamp - the input timestamp\n * @param amount - the amount to add\n * @param unit - the unit as string\n */\n add(this: DateAdapter, timestamp: number, amount: number, unit: TimeUnit): number;\n /**\n * Returns the number of `unit` between the given timestamps.\n * @param a - the input timestamp (reference)\n * @param b - the timestamp to subtract\n * @param unit - the unit as string\n */\n diff(this: DateAdapter, a: number, b: number, unit: TimeUnit): number;\n /**\n * Returns start of `unit` for the given `timestamp`.\n * @param timestamp - the input timestamp\n * @param unit - the unit as string\n * @param [weekday] - the ISO day of the week with 1 being Monday\n * and 7 being Sunday (only needed if param *unit* is `isoWeek`).\n */\n startOf(this: DateAdapter, timestamp: number, unit: TimeUnit | 'isoWeek', weekday?: number): number;\n /**\n * Returns end of `unit` for the given `timestamp`.\n * @param timestamp - the input timestamp\n * @param unit - the unit as string\n */\n endOf(this: DateAdapter, timestamp: number, unit: TimeUnit | 'isoWeek'): number;\n}\n\nfunction abstract(): T {\n throw new Error('This method is not implemented: Check that a complete date adapter is provided.');\n}\n\n/**\n * Date adapter (current used by the time scale)\n * @namespace Chart._adapters._date\n * @memberof Chart._adapters\n * @private\n */\nclass DateAdapterBase implements DateAdapter {\n\n /**\n * Override default date adapter methods.\n * Accepts type parameter to define options type.\n * @example\n * Chart._adapters._date.override<{myAdapterOption: string}>({\n * init() {\n * console.log(this.options.myAdapterOption);\n * }\n * })\n */\n static override(\n members: Partial, 'options'>>\n ) {\n Object.assign(DateAdapterBase.prototype, members);\n }\n\n readonly options: AnyObject;\n\n constructor(options: AnyObject) {\n this.options = options || {};\n }\n\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n init() {}\n\n formats(): Record {\n return abstract();\n }\n\n parse(): number | null {\n return abstract();\n }\n\n format(): string {\n return abstract();\n }\n\n add(): number {\n return abstract();\n }\n\n diff(): number {\n return abstract();\n }\n\n startOf(): number {\n return abstract();\n }\n\n endOf(): number {\n return abstract();\n }\n}\n\nexport default {\n _date: DateAdapterBase\n};\n","import DatasetController from '../core/core.datasetController.js';\nimport {\n _arrayUnique, isArray, isNullOrUndef,\n valueOrDefault, resolveObjectKey, sign, defined\n} from '../helpers/index.js';\n\nfunction getAllScaleValues(scale, type) {\n if (!scale._cache.$bar) {\n const visibleMetas = scale.getMatchingVisibleMetas(type);\n let values = [];\n\n for (let i = 0, ilen = visibleMetas.length; i < ilen; i++) {\n values = values.concat(visibleMetas[i].controller.getAllParsedValues(scale));\n }\n scale._cache.$bar = _arrayUnique(values.sort((a, b) => a - b));\n }\n return scale._cache.$bar;\n}\n\n/**\n * Computes the \"optimal\" sample size to maintain bars equally sized while preventing overlap.\n * @private\n */\nfunction computeMinSampleSize(meta) {\n const scale = meta.iScale;\n const values = getAllScaleValues(scale, meta.type);\n let min = scale._length;\n let i, ilen, curr, prev;\n const updateMinAndPrev = () => {\n if (curr === 32767 || curr === -32768) {\n // Ignore truncated pixels\n return;\n }\n if (defined(prev)) {\n // curr - prev === 0 is ignored\n min = Math.min(min, Math.abs(curr - prev) || min);\n }\n prev = curr;\n };\n\n for (i = 0, ilen = values.length; i < ilen; ++i) {\n curr = scale.getPixelForValue(values[i]);\n updateMinAndPrev();\n }\n\n prev = undefined;\n for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) {\n curr = scale.getPixelForTick(i);\n updateMinAndPrev();\n }\n\n return min;\n}\n\n/**\n * Computes an \"ideal\" category based on the absolute bar thickness or, if undefined or null,\n * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This\n * mode currently always generates bars equally sized (until we introduce scriptable options?).\n * @private\n */\nfunction computeFitCategoryTraits(index, ruler, options, stackCount) {\n const thickness = options.barThickness;\n let size, ratio;\n\n if (isNullOrUndef(thickness)) {\n size = ruler.min * options.categoryPercentage;\n ratio = options.barPercentage;\n } else {\n // When bar thickness is enforced, category and bar percentages are ignored.\n // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%')\n // and deprecate barPercentage since this value is ignored when thickness is absolute.\n size = thickness * stackCount;\n ratio = 1;\n }\n\n return {\n chunk: size / stackCount,\n ratio,\n start: ruler.pixels[index] - (size / 2)\n };\n}\n\n/**\n * Computes an \"optimal\" category that globally arranges bars side by side (no gap when\n * percentage options are 1), based on the previous and following categories. This mode\n * generates bars with different widths when data are not evenly spaced.\n * @private\n */\nfunction computeFlexCategoryTraits(index, ruler, options, stackCount) {\n const pixels = ruler.pixels;\n const curr = pixels[index];\n let prev = index > 0 ? pixels[index - 1] : null;\n let next = index < pixels.length - 1 ? pixels[index + 1] : null;\n const percent = options.categoryPercentage;\n\n if (prev === null) {\n // first data: its size is double based on the next point or,\n // if it's also the last data, we use the scale size.\n prev = curr - (next === null ? ruler.end - ruler.start : next - curr);\n }\n\n if (next === null) {\n // last data: its size is also double based on the previous point.\n next = curr + curr - prev;\n }\n\n const start = curr - (curr - Math.min(prev, next)) / 2 * percent;\n const size = Math.abs(next - prev) / 2 * percent;\n\n return {\n chunk: size / stackCount,\n ratio: options.barPercentage,\n start\n };\n}\n\nfunction parseFloatBar(entry, item, vScale, i) {\n const startValue = vScale.parse(entry[0], i);\n const endValue = vScale.parse(entry[1], i);\n const min = Math.min(startValue, endValue);\n const max = Math.max(startValue, endValue);\n let barStart = min;\n let barEnd = max;\n\n if (Math.abs(min) > Math.abs(max)) {\n barStart = max;\n barEnd = min;\n }\n\n // Store `barEnd` (furthest away from origin) as parsed value,\n // to make stacking straight forward\n item[vScale.axis] = barEnd;\n\n item._custom = {\n barStart,\n barEnd,\n start: startValue,\n end: endValue,\n min,\n max\n };\n}\n\nfunction parseValue(entry, item, vScale, i) {\n if (isArray(entry)) {\n parseFloatBar(entry, item, vScale, i);\n } else {\n item[vScale.axis] = vScale.parse(entry, i);\n }\n return item;\n}\n\nfunction parseArrayOrPrimitive(meta, data, start, count) {\n const iScale = meta.iScale;\n const vScale = meta.vScale;\n const labels = iScale.getLabels();\n const singleScale = iScale === vScale;\n const parsed = [];\n let i, ilen, item, entry;\n\n for (i = start, ilen = start + count; i < ilen; ++i) {\n entry = data[i];\n item = {};\n item[iScale.axis] = singleScale || iScale.parse(labels[i], i);\n parsed.push(parseValue(entry, item, vScale, i));\n }\n return parsed;\n}\n\nfunction isFloatBar(custom) {\n return custom && custom.barStart !== undefined && custom.barEnd !== undefined;\n}\n\nfunction barSign(size, vScale, actualBase) {\n if (size !== 0) {\n return sign(size);\n }\n return (vScale.isHorizontal() ? 1 : -1) * (vScale.min >= actualBase ? 1 : -1);\n}\n\nfunction borderProps(properties) {\n let reverse, start, end, top, bottom;\n if (properties.horizontal) {\n reverse = properties.base > properties.x;\n start = 'left';\n end = 'right';\n } else {\n reverse = properties.base < properties.y;\n start = 'bottom';\n end = 'top';\n }\n if (reverse) {\n top = 'end';\n bottom = 'start';\n } else {\n top = 'start';\n bottom = 'end';\n }\n return {start, end, reverse, top, bottom};\n}\n\nfunction setBorderSkipped(properties, options, stack, index) {\n let edge = options.borderSkipped;\n const res = {};\n\n if (!edge) {\n properties.borderSkipped = res;\n return;\n }\n\n if (edge === true) {\n properties.borderSkipped = {top: true, right: true, bottom: true, left: true};\n return;\n }\n\n const {start, end, reverse, top, bottom} = borderProps(properties);\n\n if (edge === 'middle' && stack) {\n properties.enableBorderRadius = true;\n if ((stack._top || 0) === index) {\n edge = top;\n } else if ((stack._bottom || 0) === index) {\n edge = bottom;\n } else {\n res[parseEdge(bottom, start, end, reverse)] = true;\n edge = top;\n }\n }\n\n res[parseEdge(edge, start, end, reverse)] = true;\n properties.borderSkipped = res;\n}\n\nfunction parseEdge(edge, a, b, reverse) {\n if (reverse) {\n edge = swap(edge, a, b);\n edge = startEnd(edge, b, a);\n } else {\n edge = startEnd(edge, a, b);\n }\n return edge;\n}\n\nfunction swap(orig, v1, v2) {\n return orig === v1 ? v2 : orig === v2 ? v1 : orig;\n}\n\nfunction startEnd(v, start, end) {\n return v === 'start' ? start : v === 'end' ? end : v;\n}\n\nfunction setInflateAmount(properties, {inflateAmount}, ratio) {\n properties.inflateAmount = inflateAmount === 'auto'\n ? ratio === 1 ? 0.33 : 0\n : inflateAmount;\n}\n\nexport default class BarController extends DatasetController {\n\n static id = 'bar';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: false,\n dataElementType: 'bar',\n\n categoryPercentage: 0.8,\n barPercentage: 0.9,\n grouped: true,\n\n animations: {\n numbers: {\n type: 'number',\n properties: ['x', 'y', 'base', 'width', 'height']\n }\n }\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n scales: {\n _index_: {\n type: 'category',\n offset: true,\n grid: {\n offset: true\n }\n },\n _value_: {\n type: 'linear',\n beginAtZero: true,\n }\n }\n };\n\n\n /**\n\t * Overriding primitive data parsing since we support mixed primitive/array\n\t * data for float bars\n\t * @protected\n\t */\n parsePrimitiveData(meta, data, start, count) {\n return parseArrayOrPrimitive(meta, data, start, count);\n }\n\n /**\n\t * Overriding array data parsing since we support mixed primitive/array\n\t * data for float bars\n\t * @protected\n\t */\n parseArrayData(meta, data, start, count) {\n return parseArrayOrPrimitive(meta, data, start, count);\n }\n\n /**\n\t * Overriding object data parsing since we support mixed primitive/array\n\t * value-scale data for float bars\n\t * @protected\n\t */\n parseObjectData(meta, data, start, count) {\n const {iScale, vScale} = meta;\n const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing;\n const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey;\n const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey;\n const parsed = [];\n let i, ilen, item, obj;\n for (i = start, ilen = start + count; i < ilen; ++i) {\n obj = data[i];\n item = {};\n item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i);\n parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i));\n }\n return parsed;\n }\n\n /**\n\t * @protected\n\t */\n updateRangeFromParsed(range, scale, parsed, stack) {\n super.updateRangeFromParsed(range, scale, parsed, stack);\n const custom = parsed._custom;\n if (custom && scale === this._cachedMeta.vScale) {\n // float bar: only one end of the bar is considered by `super`\n range.min = Math.min(range.min, custom.min);\n range.max = Math.max(range.max, custom.max);\n }\n }\n\n /**\n\t * @return {number|boolean}\n\t * @protected\n\t */\n getMaxOverflow() {\n return 0;\n }\n\n /**\n\t * @protected\n\t */\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const {iScale, vScale} = meta;\n const parsed = this.getParsed(index);\n const custom = parsed._custom;\n const value = isFloatBar(custom)\n ? '[' + custom.start + ', ' + custom.end + ']'\n : '' + vScale.getLabelForValue(parsed[vScale.axis]);\n\n return {\n label: '' + iScale.getLabelForValue(parsed[iScale.axis]),\n value\n };\n }\n\n initialize() {\n this.enableOptionSharing = true;\n\n super.initialize();\n\n const meta = this._cachedMeta;\n meta.stack = this.getDataset().stack;\n }\n\n update(mode) {\n const meta = this._cachedMeta;\n this.updateElements(meta.data, 0, meta.data.length, mode);\n }\n\n updateElements(bars, start, count, mode) {\n const reset = mode === 'reset';\n const {index, _cachedMeta: {vScale}} = this;\n const base = vScale.getBasePixel();\n const horizontal = vScale.isHorizontal();\n const ruler = this._getRuler();\n const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);\n\n for (let i = start; i < start + count; i++) {\n const parsed = this.getParsed(i);\n const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : this._calculateBarValuePixels(i);\n const ipixels = this._calculateBarIndexPixels(i, ruler);\n const stack = (parsed._stacks || {})[vScale.axis];\n\n const properties = {\n horizontal,\n base: vpixels.base,\n enableBorderRadius: !stack || isFloatBar(parsed._custom) || (index === stack._top || index === stack._bottom),\n x: horizontal ? vpixels.head : ipixels.center,\n y: horizontal ? ipixels.center : vpixels.head,\n height: horizontal ? ipixels.size : Math.abs(vpixels.size),\n width: horizontal ? Math.abs(vpixels.size) : ipixels.size\n };\n\n if (includeOptions) {\n properties.options = sharedOptions || this.resolveDataElementOptions(i, bars[i].active ? 'active' : mode);\n }\n const options = properties.options || bars[i].options;\n setBorderSkipped(properties, options, stack, index);\n setInflateAmount(properties, options, ruler.ratio);\n this.updateElement(bars[i], i, properties, mode);\n }\n }\n\n /**\n\t * Returns the stacks based on groups and bar visibility.\n\t * @param {number} [last] - The dataset index\n\t * @param {number} [dataIndex] - The data index of the ruler\n\t * @returns {string[]} The list of stack IDs\n\t * @private\n\t */\n _getStacks(last, dataIndex) {\n const {iScale} = this._cachedMeta;\n const metasets = iScale.getMatchingVisibleMetas(this._type)\n .filter(meta => meta.controller.options.grouped);\n const stacked = iScale.options.stacked;\n const stacks = [];\n const currentParsed = this._cachedMeta.controller.getParsed(dataIndex);\n const iScaleValue = currentParsed && currentParsed[iScale.axis];\n\n const skipNull = (meta) => {\n const parsed = meta._parsed.find(item => item[iScale.axis] === iScaleValue);\n const val = parsed && parsed[meta.vScale.axis];\n\n if (isNullOrUndef(val) || isNaN(val)) {\n return true;\n }\n };\n\n for (const meta of metasets) {\n if (dataIndex !== undefined && skipNull(meta)) {\n continue;\n }\n\n // stacked | meta.stack\n // | found | not found | undefined\n // false | x | x | x\n // true | | x |\n // undefined | | x | x\n if (stacked === false || stacks.indexOf(meta.stack) === -1 ||\n\t\t\t\t(stacked === undefined && meta.stack === undefined)) {\n stacks.push(meta.stack);\n }\n if (meta.index === last) {\n break;\n }\n }\n\n // No stacks? that means there is no visible data. Let's still initialize an `undefined`\n // stack where possible invisible bars will be located.\n // https://github.com/chartjs/Chart.js/issues/6368\n if (!stacks.length) {\n stacks.push(undefined);\n }\n\n return stacks;\n }\n\n /**\n\t * Returns the effective number of stacks based on groups and bar visibility.\n\t * @private\n\t */\n _getStackCount(index) {\n return this._getStacks(undefined, index).length;\n }\n\n /**\n\t * Returns the stack index for the given dataset based on groups and bar visibility.\n\t * @param {number} [datasetIndex] - The dataset index\n\t * @param {string} [name] - The stack name to find\n * @param {number} [dataIndex]\n\t * @returns {number} The stack index\n\t * @private\n\t */\n _getStackIndex(datasetIndex, name, dataIndex) {\n const stacks = this._getStacks(datasetIndex, dataIndex);\n const index = (name !== undefined)\n ? stacks.indexOf(name)\n : -1; // indexOf returns -1 if element is not present\n\n return (index === -1)\n ? stacks.length - 1\n : index;\n }\n\n /**\n\t * @private\n\t */\n _getRuler() {\n const opts = this.options;\n const meta = this._cachedMeta;\n const iScale = meta.iScale;\n const pixels = [];\n let i, ilen;\n\n for (i = 0, ilen = meta.data.length; i < ilen; ++i) {\n pixels.push(iScale.getPixelForValue(this.getParsed(i)[iScale.axis], i));\n }\n\n const barThickness = opts.barThickness;\n const min = barThickness || computeMinSampleSize(meta);\n\n return {\n min,\n pixels,\n start: iScale._startPixel,\n end: iScale._endPixel,\n stackCount: this._getStackCount(),\n scale: iScale,\n grouped: opts.grouped,\n // bar thickness ratio used for non-grouped bars\n ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage\n };\n }\n\n /**\n\t * Note: pixel values are not clamped to the scale area.\n\t * @private\n\t */\n _calculateBarValuePixels(index) {\n const {_cachedMeta: {vScale, _stacked, index: datasetIndex}, options: {base: baseValue, minBarLength}} = this;\n const actualBase = baseValue || 0;\n const parsed = this.getParsed(index);\n const custom = parsed._custom;\n const floating = isFloatBar(custom);\n let value = parsed[vScale.axis];\n let start = 0;\n let length = _stacked ? this.applyStack(vScale, parsed, _stacked) : value;\n let head, size;\n\n if (length !== value) {\n start = length - value;\n length = value;\n }\n\n if (floating) {\n value = custom.barStart;\n length = custom.barEnd - custom.barStart;\n // bars crossing origin are not stacked\n if (value !== 0 && sign(value) !== sign(custom.barEnd)) {\n start = 0;\n }\n start += value;\n }\n\n const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start;\n let base = vScale.getPixelForValue(startValue);\n\n if (this.chart.getDataVisibility(index)) {\n head = vScale.getPixelForValue(start + length);\n } else {\n // When not visible, no height\n head = base;\n }\n\n size = head - base;\n\n if (Math.abs(size) < minBarLength) {\n size = barSign(size, vScale, actualBase) * minBarLength;\n if (value === actualBase) {\n base -= size / 2;\n }\n const startPixel = vScale.getPixelForDecimal(0);\n const endPixel = vScale.getPixelForDecimal(1);\n const min = Math.min(startPixel, endPixel);\n const max = Math.max(startPixel, endPixel);\n base = Math.max(Math.min(base, max), min);\n head = base + size;\n\n if (_stacked && !floating) {\n // visual data coordinates after applying minBarLength\n parsed._stacks[vScale.axis]._visualValues[datasetIndex] = vScale.getValueForPixel(head) - vScale.getValueForPixel(base);\n }\n }\n\n if (base === vScale.getPixelForValue(actualBase)) {\n const halfGrid = sign(size) * vScale.getLineWidthForValue(actualBase) / 2;\n base += halfGrid;\n size -= halfGrid;\n }\n\n return {\n size,\n base,\n head,\n center: head + size / 2\n };\n }\n\n /**\n\t * @private\n\t */\n _calculateBarIndexPixels(index, ruler) {\n const scale = ruler.scale;\n const options = this.options;\n const skipNull = options.skipNull;\n const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity);\n let center, size;\n if (ruler.grouped) {\n const stackCount = skipNull ? this._getStackCount(index) : ruler.stackCount;\n const range = options.barThickness === 'flex'\n ? computeFlexCategoryTraits(index, ruler, options, stackCount)\n : computeFitCategoryTraits(index, ruler, options, stackCount);\n\n const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined);\n center = range.start + (range.chunk * stackIndex) + (range.chunk / 2);\n size = Math.min(maxBarThickness, range.chunk * range.ratio);\n } else {\n // For non-grouped bar charts, exact pixel values are used\n center = scale.getPixelForValue(this.getParsed(index)[scale.axis], index);\n size = Math.min(maxBarThickness, ruler.min * ruler.ratio);\n }\n\n return {\n base: center - size / 2,\n head: center + size / 2,\n center,\n size\n };\n }\n\n draw() {\n const meta = this._cachedMeta;\n const vScale = meta.vScale;\n const rects = meta.data;\n const ilen = rects.length;\n let i = 0;\n\n for (; i < ilen; ++i) {\n if (this.getParsed(i)[vScale.axis] !== null && !rects[i].hidden) {\n rects[i].draw(this._ctx);\n }\n }\n }\n\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {isObject, resolveObjectKey, toPercentage, toDimension, valueOrDefault} from '../helpers/helpers.core.js';\nimport {formatNumber} from '../helpers/helpers.intl.js';\nimport {toRadians, PI, TAU, HALF_PI, _angleBetween} from '../helpers/helpers.math.js';\n\n/**\n * @typedef { import('../core/core.controller.js').default } Chart\n */\n\nfunction getRatioAndOffset(rotation, circumference, cutout) {\n let ratioX = 1;\n let ratioY = 1;\n let offsetX = 0;\n let offsetY = 0;\n // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc\n if (circumference < TAU) {\n const startAngle = rotation;\n const endAngle = startAngle + circumference;\n const startX = Math.cos(startAngle);\n const startY = Math.sin(startAngle);\n const endX = Math.cos(endAngle);\n const endY = Math.sin(endAngle);\n const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout);\n const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout);\n const maxX = calcMax(0, startX, endX);\n const maxY = calcMax(HALF_PI, startY, endY);\n const minX = calcMin(PI, startX, endX);\n const minY = calcMin(PI + HALF_PI, startY, endY);\n ratioX = (maxX - minX) / 2;\n ratioY = (maxY - minY) / 2;\n offsetX = -(maxX + minX) / 2;\n offsetY = -(maxY + minY) / 2;\n }\n return {ratioX, ratioY, offsetX, offsetY};\n}\n\nexport default class DoughnutController extends DatasetController {\n\n static id = 'doughnut';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: false,\n dataElementType: 'arc',\n animation: {\n // Boolean - Whether we animate the rotation of the Doughnut\n animateRotate: true,\n // Boolean - Whether we animate scaling the Doughnut from the centre\n animateScale: false\n },\n animations: {\n numbers: {\n type: 'number',\n properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing']\n },\n },\n // The percentage of the chart that we cut out of the middle.\n cutout: '50%',\n\n // The rotation of the chart, where the first data arc begins.\n rotation: 0,\n\n // The total circumference of the chart.\n circumference: 360,\n\n // The outer radius of the chart\n radius: '100%',\n\n // Spacing between arcs\n spacing: 0,\n\n indexAxis: 'r',\n };\n\n static descriptors = {\n _scriptable: (name) => name !== 'spacing',\n _indexable: (name) => name !== 'spacing' && !name.startsWith('borderDash') && !name.startsWith('hoverBorderDash'),\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n aspectRatio: 1,\n\n // Need to override these to give a nice default\n plugins: {\n legend: {\n labels: {\n generateLabels(chart) {\n const data = chart.data;\n if (data.labels.length && data.datasets.length) {\n const {labels: {pointStyle, color}} = chart.legend.options;\n\n return data.labels.map((label, i) => {\n const meta = chart.getDatasetMeta(0);\n const style = meta.controller.getStyle(i);\n\n return {\n text: label,\n fillStyle: style.backgroundColor,\n strokeStyle: style.borderColor,\n fontColor: color,\n lineWidth: style.borderWidth,\n pointStyle: pointStyle,\n hidden: !chart.getDataVisibility(i),\n\n // Extra data used for toggling the correct item\n index: i\n };\n });\n }\n return [];\n }\n },\n\n onClick(e, legendItem, legend) {\n legend.chart.toggleDataVisibility(legendItem.index);\n legend.chart.update();\n }\n }\n }\n };\n\n constructor(chart, datasetIndex) {\n super(chart, datasetIndex);\n\n this.enableOptionSharing = true;\n this.innerRadius = undefined;\n this.outerRadius = undefined;\n this.offsetX = undefined;\n this.offsetY = undefined;\n }\n\n linkScales() {}\n\n /**\n\t * Override data parsing, since we are not using scales\n\t */\n parse(start, count) {\n const data = this.getDataset().data;\n const meta = this._cachedMeta;\n\n if (this._parsing === false) {\n meta._parsed = data;\n } else {\n let getter = (i) => +data[i];\n\n if (isObject(data[start])) {\n const {key = 'value'} = this._parsing;\n getter = (i) => +resolveObjectKey(data[i], key);\n }\n\n let i, ilen;\n for (i = start, ilen = start + count; i < ilen; ++i) {\n meta._parsed[i] = getter(i);\n }\n }\n }\n\n /**\n\t * @private\n\t */\n _getRotation() {\n return toRadians(this.options.rotation - 90);\n }\n\n /**\n\t * @private\n\t */\n _getCircumference() {\n return toRadians(this.options.circumference);\n }\n\n /**\n\t * Get the maximal rotation & circumference extents\n\t * across all visible datasets.\n\t */\n _getRotationExtents() {\n let min = TAU;\n let max = -TAU;\n\n for (let i = 0; i < this.chart.data.datasets.length; ++i) {\n if (this.chart.isDatasetVisible(i) && this.chart.getDatasetMeta(i).type === this._type) {\n const controller = this.chart.getDatasetMeta(i).controller;\n const rotation = controller._getRotation();\n const circumference = controller._getCircumference();\n\n min = Math.min(min, rotation);\n max = Math.max(max, rotation + circumference);\n }\n }\n\n return {\n rotation: min,\n circumference: max - min,\n };\n }\n\n /**\n\t * @param {string} mode\n\t */\n update(mode) {\n const chart = this.chart;\n const {chartArea} = chart;\n const meta = this._cachedMeta;\n const arcs = meta.data;\n const spacing = this.getMaxBorderWidth() + this.getMaxOffset(arcs) + this.options.spacing;\n const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0);\n const cutout = Math.min(toPercentage(this.options.cutout, maxSize), 1);\n const chartWeight = this._getRingWeight(this.index);\n\n // Compute the maximal rotation & circumference limits.\n // If we only consider our dataset, this can cause problems when two datasets\n // are both less than a circle with different rotations (starting angles)\n const {circumference, rotation} = this._getRotationExtents();\n const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout);\n const maxWidth = (chartArea.width - spacing) / ratioX;\n const maxHeight = (chartArea.height - spacing) / ratioY;\n const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);\n const outerRadius = toDimension(this.options.radius, maxRadius);\n const innerRadius = Math.max(outerRadius * cutout, 0);\n const radiusLength = (outerRadius - innerRadius) / this._getVisibleDatasetWeightTotal();\n this.offsetX = offsetX * outerRadius;\n this.offsetY = offsetY * outerRadius;\n\n meta.total = this.calculateTotal();\n\n this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index);\n this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight, 0);\n\n this.updateElements(arcs, 0, arcs.length, mode);\n }\n\n /**\n * @private\n */\n _circumference(i, reset) {\n const opts = this.options;\n const meta = this._cachedMeta;\n const circumference = this._getCircumference();\n if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null || meta.data[i].hidden) {\n return 0;\n }\n return this.calculateCircumference(meta._parsed[i] * circumference / TAU);\n }\n\n updateElements(arcs, start, count, mode) {\n const reset = mode === 'reset';\n const chart = this.chart;\n const chartArea = chart.chartArea;\n const opts = chart.options;\n const animationOpts = opts.animation;\n const centerX = (chartArea.left + chartArea.right) / 2;\n const centerY = (chartArea.top + chartArea.bottom) / 2;\n const animateScale = reset && animationOpts.animateScale;\n const innerRadius = animateScale ? 0 : this.innerRadius;\n const outerRadius = animateScale ? 0 : this.outerRadius;\n const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);\n let startAngle = this._getRotation();\n let i;\n\n for (i = 0; i < start; ++i) {\n startAngle += this._circumference(i, reset);\n }\n\n for (i = start; i < start + count; ++i) {\n const circumference = this._circumference(i, reset);\n const arc = arcs[i];\n const properties = {\n x: centerX + this.offsetX,\n y: centerY + this.offsetY,\n startAngle,\n endAngle: startAngle + circumference,\n circumference,\n outerRadius,\n innerRadius\n };\n if (includeOptions) {\n properties.options = sharedOptions || this.resolveDataElementOptions(i, arc.active ? 'active' : mode);\n }\n startAngle += circumference;\n\n this.updateElement(arc, i, properties, mode);\n }\n }\n\n calculateTotal() {\n const meta = this._cachedMeta;\n const metaData = meta.data;\n let total = 0;\n let i;\n\n for (i = 0; i < metaData.length; i++) {\n const value = meta._parsed[i];\n if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i) && !metaData[i].hidden) {\n total += Math.abs(value);\n }\n }\n\n return total;\n }\n\n calculateCircumference(value) {\n const total = this._cachedMeta.total;\n if (total > 0 && !isNaN(value)) {\n return TAU * (Math.abs(value) / total);\n }\n return 0;\n }\n\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const chart = this.chart;\n const labels = chart.data.labels || [];\n const value = formatNumber(meta._parsed[index], chart.options.locale);\n\n return {\n label: labels[index] || '',\n value,\n };\n }\n\n getMaxBorderWidth(arcs) {\n let max = 0;\n const chart = this.chart;\n let i, ilen, meta, controller, options;\n\n if (!arcs) {\n // Find the outmost visible dataset\n for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {\n if (chart.isDatasetVisible(i)) {\n meta = chart.getDatasetMeta(i);\n arcs = meta.data;\n controller = meta.controller;\n break;\n }\n }\n }\n\n if (!arcs) {\n return 0;\n }\n\n for (i = 0, ilen = arcs.length; i < ilen; ++i) {\n options = controller.resolveDataElementOptions(i);\n if (options.borderAlign !== 'inner') {\n max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0);\n }\n }\n return max;\n }\n\n getMaxOffset(arcs) {\n let max = 0;\n\n for (let i = 0, ilen = arcs.length; i < ilen; ++i) {\n const options = this.resolveDataElementOptions(i);\n max = Math.max(max, options.offset || 0, options.hoverOffset || 0);\n }\n return max;\n }\n\n /**\n\t * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly\n\t * @private\n\t */\n _getRingWeightOffset(datasetIndex) {\n let ringWeightOffset = 0;\n\n for (let i = 0; i < datasetIndex; ++i) {\n if (this.chart.isDatasetVisible(i)) {\n ringWeightOffset += this._getRingWeight(i);\n }\n }\n\n return ringWeightOffset;\n }\n\n /**\n\t * @private\n\t */\n _getRingWeight(datasetIndex) {\n return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0);\n }\n\n /**\n\t * Returns the sum of all visible data set weights.\n\t * @private\n\t */\n _getVisibleDatasetWeightTotal() {\n return this._getRingWeightOffset(this.chart.data.datasets.length) || 1;\n }\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {toRadians, PI, formatNumber, _parseObjectDataRadialScale} from '../helpers/index.js';\n\nexport default class PolarAreaController extends DatasetController {\n\n static id = 'polarArea';\n\n /**\n * @type {any}\n */\n static defaults = {\n dataElementType: 'arc',\n animation: {\n animateRotate: true,\n animateScale: true\n },\n animations: {\n numbers: {\n type: 'number',\n properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius']\n },\n },\n indexAxis: 'r',\n startAngle: 0,\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n aspectRatio: 1,\n\n plugins: {\n legend: {\n labels: {\n generateLabels(chart) {\n const data = chart.data;\n if (data.labels.length && data.datasets.length) {\n const {labels: {pointStyle, color}} = chart.legend.options;\n\n return data.labels.map((label, i) => {\n const meta = chart.getDatasetMeta(0);\n const style = meta.controller.getStyle(i);\n\n return {\n text: label,\n fillStyle: style.backgroundColor,\n strokeStyle: style.borderColor,\n fontColor: color,\n lineWidth: style.borderWidth,\n pointStyle: pointStyle,\n hidden: !chart.getDataVisibility(i),\n\n // Extra data used for toggling the correct item\n index: i\n };\n });\n }\n return [];\n }\n },\n\n onClick(e, legendItem, legend) {\n legend.chart.toggleDataVisibility(legendItem.index);\n legend.chart.update();\n }\n }\n },\n\n scales: {\n r: {\n type: 'radialLinear',\n angleLines: {\n display: false\n },\n beginAtZero: true,\n grid: {\n circular: true\n },\n pointLabels: {\n display: false\n },\n startAngle: 0\n }\n }\n };\n\n constructor(chart, datasetIndex) {\n super(chart, datasetIndex);\n\n this.innerRadius = undefined;\n this.outerRadius = undefined;\n }\n\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const chart = this.chart;\n const labels = chart.data.labels || [];\n const value = formatNumber(meta._parsed[index].r, chart.options.locale);\n\n return {\n label: labels[index] || '',\n value,\n };\n }\n\n parseObjectData(meta, data, start, count) {\n return _parseObjectDataRadialScale.bind(this)(meta, data, start, count);\n }\n\n update(mode) {\n const arcs = this._cachedMeta.data;\n\n this._updateRadius();\n this.updateElements(arcs, 0, arcs.length, mode);\n }\n\n /**\n * @protected\n */\n getMinMax() {\n const meta = this._cachedMeta;\n const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY};\n\n meta.data.forEach((element, index) => {\n const parsed = this.getParsed(index).r;\n\n if (!isNaN(parsed) && this.chart.getDataVisibility(index)) {\n if (parsed < range.min) {\n range.min = parsed;\n }\n\n if (parsed > range.max) {\n range.max = parsed;\n }\n }\n });\n\n return range;\n }\n\n /**\n\t * @private\n\t */\n _updateRadius() {\n const chart = this.chart;\n const chartArea = chart.chartArea;\n const opts = chart.options;\n const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);\n\n const outerRadius = Math.max(minSize / 2, 0);\n const innerRadius = Math.max(opts.cutoutPercentage ? (outerRadius / 100) * (opts.cutoutPercentage) : 1, 0);\n const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount();\n\n this.outerRadius = outerRadius - (radiusLength * this.index);\n this.innerRadius = this.outerRadius - radiusLength;\n }\n\n updateElements(arcs, start, count, mode) {\n const reset = mode === 'reset';\n const chart = this.chart;\n const opts = chart.options;\n const animationOpts = opts.animation;\n const scale = this._cachedMeta.rScale;\n const centerX = scale.xCenter;\n const centerY = scale.yCenter;\n const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI;\n let angle = datasetStartAngle;\n let i;\n\n const defaultAngle = 360 / this.countVisibleElements();\n\n for (i = 0; i < start; ++i) {\n angle += this._computeAngle(i, mode, defaultAngle);\n }\n for (i = start; i < start + count; i++) {\n const arc = arcs[i];\n let startAngle = angle;\n let endAngle = angle + this._computeAngle(i, mode, defaultAngle);\n let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(this.getParsed(i).r) : 0;\n angle = endAngle;\n\n if (reset) {\n if (animationOpts.animateScale) {\n outerRadius = 0;\n }\n if (animationOpts.animateRotate) {\n startAngle = endAngle = datasetStartAngle;\n }\n }\n\n const properties = {\n x: centerX,\n y: centerY,\n innerRadius: 0,\n outerRadius,\n startAngle,\n endAngle,\n options: this.resolveDataElementOptions(i, arc.active ? 'active' : mode)\n };\n\n this.updateElement(arc, i, properties, mode);\n }\n }\n\n countVisibleElements() {\n const meta = this._cachedMeta;\n let count = 0;\n\n meta.data.forEach((element, index) => {\n if (!isNaN(this.getParsed(index).r) && this.chart.getDataVisibility(index)) {\n count++;\n }\n });\n\n return count;\n }\n\n /**\n\t * @private\n\t */\n _computeAngle(index, mode, defaultAngle) {\n return this.chart.getDataVisibility(index)\n ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle)\n : 0;\n }\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {valueOrDefault} from '../helpers/helpers.core.js';\n\nexport default class BubbleController extends DatasetController {\n\n static id = 'bubble';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: false,\n dataElementType: 'point',\n\n animations: {\n numbers: {\n type: 'number',\n properties: ['x', 'y', 'borderWidth', 'radius']\n }\n }\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n scales: {\n x: {\n type: 'linear'\n },\n y: {\n type: 'linear'\n }\n }\n };\n\n initialize() {\n this.enableOptionSharing = true;\n super.initialize();\n }\n\n /**\n\t * Parse array of primitive values\n\t * @protected\n\t */\n parsePrimitiveData(meta, data, start, count) {\n const parsed = super.parsePrimitiveData(meta, data, start, count);\n for (let i = 0; i < parsed.length; i++) {\n parsed[i]._custom = this.resolveDataElementOptions(i + start).radius;\n }\n return parsed;\n }\n\n /**\n\t * Parse array of arrays\n\t * @protected\n\t */\n parseArrayData(meta, data, start, count) {\n const parsed = super.parseArrayData(meta, data, start, count);\n for (let i = 0; i < parsed.length; i++) {\n const item = data[start + i];\n parsed[i]._custom = valueOrDefault(item[2], this.resolveDataElementOptions(i + start).radius);\n }\n return parsed;\n }\n\n /**\n\t * Parse array of objects\n\t * @protected\n\t */\n parseObjectData(meta, data, start, count) {\n const parsed = super.parseObjectData(meta, data, start, count);\n for (let i = 0; i < parsed.length; i++) {\n const item = data[start + i];\n parsed[i]._custom = valueOrDefault(item && item.r && +item.r, this.resolveDataElementOptions(i + start).radius);\n }\n return parsed;\n }\n\n /**\n\t * @protected\n\t */\n getMaxOverflow() {\n const data = this._cachedMeta.data;\n\n let max = 0;\n for (let i = data.length - 1; i >= 0; --i) {\n max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2);\n }\n return max > 0 && max;\n }\n\n /**\n\t * @protected\n\t */\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const labels = this.chart.data.labels || [];\n const {xScale, yScale} = meta;\n const parsed = this.getParsed(index);\n const x = xScale.getLabelForValue(parsed.x);\n const y = yScale.getLabelForValue(parsed.y);\n const r = parsed._custom;\n\n return {\n label: labels[index] || '',\n value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')'\n };\n }\n\n update(mode) {\n const points = this._cachedMeta.data;\n\n // Update Points\n this.updateElements(points, 0, points.length, mode);\n }\n\n updateElements(points, start, count, mode) {\n const reset = mode === 'reset';\n const {iScale, vScale} = this._cachedMeta;\n const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);\n const iAxis = iScale.axis;\n const vAxis = vScale.axis;\n\n for (let i = start; i < start + count; i++) {\n const point = points[i];\n const parsed = !reset && this.getParsed(i);\n const properties = {};\n const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]);\n const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]);\n\n properties.skip = isNaN(iPixel) || isNaN(vPixel);\n\n if (includeOptions) {\n properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);\n\n if (reset) {\n properties.options.radius = 0;\n }\n }\n\n this.updateElement(point, i, properties, mode);\n }\n }\n\n /**\n\t * @param {number} index\n\t * @param {string} [mode]\n\t * @protected\n\t */\n resolveDataElementOptions(index, mode) {\n const parsed = this.getParsed(index);\n let values = super.resolveDataElementOptions(index, mode);\n\n // In case values were cached (and thus frozen), we need to clone the values\n if (values.$shared) {\n values = Object.assign({}, values, {$shared: false});\n }\n\n // Custom radius resolution\n const radius = values.radius;\n if (mode !== 'active') {\n values.radius = 0;\n }\n values.radius += valueOrDefault(parsed && parsed._custom, radius);\n\n return values;\n }\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {isNullOrUndef} from '../helpers/index.js';\nimport {isNumber} from '../helpers/helpers.math.js';\nimport {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js';\n\nexport default class LineController extends DatasetController {\n\n static id = 'line';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: 'line',\n dataElementType: 'point',\n\n showLine: true,\n spanGaps: false,\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n scales: {\n _index_: {\n type: 'category',\n },\n _value_: {\n type: 'linear',\n },\n }\n };\n\n initialize() {\n this.enableOptionSharing = true;\n this.supportsDecimation = true;\n super.initialize();\n }\n\n update(mode) {\n const meta = this._cachedMeta;\n const {dataset: line, data: points = [], _dataset} = meta;\n // @ts-ignore\n const animationsDisabled = this.chart._animationsDisabled;\n let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled);\n\n this._drawStart = start;\n this._drawCount = count;\n\n if (_scaleRangesChanged(meta)) {\n start = 0;\n count = points.length;\n }\n\n // Update Line\n line._chart = this.chart;\n line._datasetIndex = this.index;\n line._decimated = !!_dataset._decimated;\n line.points = points;\n\n const options = this.resolveDatasetElementOptions(mode);\n if (!this.options.showLine) {\n options.borderWidth = 0;\n }\n options.segment = this.options.segment;\n this.updateElement(line, undefined, {\n animated: !animationsDisabled,\n options\n }, mode);\n\n // Update Points\n this.updateElements(points, start, count, mode);\n }\n\n updateElements(points, start, count, mode) {\n const reset = mode === 'reset';\n const {iScale, vScale, _stacked, _dataset} = this._cachedMeta;\n const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);\n const iAxis = iScale.axis;\n const vAxis = vScale.axis;\n const {spanGaps, segment} = this.options;\n const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;\n const directUpdate = this.chart._animationsDisabled || reset || mode === 'none';\n const end = start + count;\n const pointsCount = points.length;\n let prevParsed = start > 0 && this.getParsed(start - 1);\n\n for (let i = 0; i < pointsCount; ++i) {\n const point = points[i];\n const properties = directUpdate ? point : {};\n\n if (i < start || i >= end) {\n properties.skip = true;\n continue;\n }\n\n const parsed = this.getParsed(i);\n const nullData = isNullOrUndef(parsed[vAxis]);\n const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i);\n const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i);\n\n properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData;\n properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength;\n if (segment) {\n properties.parsed = parsed;\n properties.raw = _dataset.data[i];\n }\n\n if (includeOptions) {\n properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);\n }\n\n if (!directUpdate) {\n this.updateElement(point, i, properties, mode);\n }\n\n prevParsed = parsed;\n }\n }\n\n /**\n\t * @protected\n\t */\n getMaxOverflow() {\n const meta = this._cachedMeta;\n const dataset = meta.dataset;\n const border = dataset.options && dataset.options.borderWidth || 0;\n const data = meta.data || [];\n if (!data.length) {\n return border;\n }\n const firstPoint = data[0].size(this.resolveDataElementOptions(0));\n const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1));\n return Math.max(border, firstPoint, lastPoint) / 2;\n }\n\n draw() {\n const meta = this._cachedMeta;\n meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis);\n super.draw();\n }\n}\n","import DoughnutController from './controller.doughnut.js';\n\n// Pie charts are Doughnut chart with different defaults\nexport default class PieController extends DoughnutController {\n\n static id = 'pie';\n\n /**\n * @type {any}\n */\n static defaults = {\n // The percentage of the chart that we cut out of the middle.\n cutout: 0,\n\n // The rotation of the chart, where the first data arc begins.\n rotation: 0,\n\n // The total circumference of the chart.\n circumference: 360,\n\n // The outer radius of the chart\n radius: '100%'\n };\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {_parseObjectDataRadialScale} from '../helpers/index.js';\n\nexport default class RadarController extends DatasetController {\n\n static id = 'radar';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: 'line',\n dataElementType: 'point',\n indexAxis: 'r',\n showLine: true,\n elements: {\n line: {\n fill: 'start'\n }\n },\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n aspectRatio: 1,\n\n scales: {\n r: {\n type: 'radialLinear',\n }\n }\n };\n\n /**\n\t * @protected\n\t */\n getLabelAndValue(index) {\n const vScale = this._cachedMeta.vScale;\n const parsed = this.getParsed(index);\n\n return {\n label: vScale.getLabels()[index],\n value: '' + vScale.getLabelForValue(parsed[vScale.axis])\n };\n }\n\n parseObjectData(meta, data, start, count) {\n return _parseObjectDataRadialScale.bind(this)(meta, data, start, count);\n }\n\n update(mode) {\n const meta = this._cachedMeta;\n const line = meta.dataset;\n const points = meta.data || [];\n const labels = meta.iScale.getLabels();\n\n // Update Line\n line.points = points;\n // In resize mode only point locations change, so no need to set the points or options.\n if (mode !== 'resize') {\n const options = this.resolveDatasetElementOptions(mode);\n if (!this.options.showLine) {\n options.borderWidth = 0;\n }\n\n const properties = {\n _loop: true,\n _fullLoop: labels.length === points.length,\n options\n };\n\n this.updateElement(line, undefined, properties, mode);\n }\n\n // Update Points\n this.updateElements(points, 0, points.length, mode);\n }\n\n updateElements(points, start, count, mode) {\n const scale = this._cachedMeta.rScale;\n const reset = mode === 'reset';\n\n for (let i = start; i < start + count; i++) {\n const point = points[i];\n const options = this.resolveDataElementOptions(i, point.active ? 'active' : mode);\n const pointPosition = scale.getPointPositionForValue(i, this.getParsed(i).r);\n\n const x = reset ? scale.xCenter : pointPosition.x;\n const y = reset ? scale.yCenter : pointPosition.y;\n\n const properties = {\n x,\n y,\n angle: pointPosition.angle,\n skip: isNaN(x) || isNaN(y),\n options\n };\n\n this.updateElement(point, i, properties, mode);\n }\n }\n}\n","import DatasetController from '../core/core.datasetController.js';\nimport {isNullOrUndef} from '../helpers/index.js';\nimport {isNumber} from '../helpers/helpers.math.js';\nimport {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js';\n\nexport default class ScatterController extends DatasetController {\n\n static id = 'scatter';\n\n /**\n * @type {any}\n */\n static defaults = {\n datasetElementType: false,\n dataElementType: 'point',\n showLine: false,\n fill: false\n };\n\n /**\n * @type {any}\n */\n static overrides = {\n\n interaction: {\n mode: 'point'\n },\n\n scales: {\n x: {\n type: 'linear'\n },\n y: {\n type: 'linear'\n }\n }\n };\n\n /**\n\t * @protected\n\t */\n getLabelAndValue(index) {\n const meta = this._cachedMeta;\n const labels = this.chart.data.labels || [];\n const {xScale, yScale} = meta;\n const parsed = this.getParsed(index);\n const x = xScale.getLabelForValue(parsed.x);\n const y = yScale.getLabelForValue(parsed.y);\n\n return {\n label: labels[index] || '',\n value: '(' + x + ', ' + y + ')'\n };\n }\n\n update(mode) {\n const meta = this._cachedMeta;\n const {data: points = []} = meta;\n // @ts-ignore\n const animationsDisabled = this.chart._animationsDisabled;\n let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled);\n\n this._drawStart = start;\n this._drawCount = count;\n\n if (_scaleRangesChanged(meta)) {\n start = 0;\n count = points.length;\n }\n\n if (this.options.showLine) {\n\n // https://github.com/chartjs/Chart.js/issues/11333\n if (!this.datasetElementType) {\n this.addElements();\n }\n const {dataset: line, _dataset} = meta;\n\n // Update Line\n line._chart = this.chart;\n line._datasetIndex = this.index;\n line._decimated = !!_dataset._decimated;\n line.points = points;\n\n const options = this.resolveDatasetElementOptions(mode);\n options.segment = this.options.segment;\n this.updateElement(line, undefined, {\n animated: !animationsDisabled,\n options\n }, mode);\n } else if (this.datasetElementType) {\n // https://github.com/chartjs/Chart.js/issues/11333\n delete meta.dataset;\n this.datasetElementType = false;\n }\n\n // Update Points\n this.updateElements(points, start, count, mode);\n }\n\n addElements() {\n const {showLine} = this.options;\n\n if (!this.datasetElementType && showLine) {\n this.datasetElementType = this.chart.registry.getElement('line');\n }\n\n super.addElements();\n }\n\n updateElements(points, start, count, mode) {\n const reset = mode === 'reset';\n const {iScale, vScale, _stacked, _dataset} = this._cachedMeta;\n const firstOpts = this.resolveDataElementOptions(start, mode);\n const sharedOptions = this.getSharedOptions(firstOpts);\n const includeOptions = this.includeOptions(mode, sharedOptions);\n const iAxis = iScale.axis;\n const vAxis = vScale.axis;\n const {spanGaps, segment} = this.options;\n const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;\n const directUpdate = this.chart._animationsDisabled || reset || mode === 'none';\n let prevParsed = start > 0 && this.getParsed(start - 1);\n\n for (let i = start; i < start + count; ++i) {\n const point = points[i];\n const parsed = this.getParsed(i);\n const properties = directUpdate ? point : {};\n const nullData = isNullOrUndef(parsed[vAxis]);\n const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i);\n const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i);\n\n properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData;\n properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength;\n if (segment) {\n properties.parsed = parsed;\n properties.raw = _dataset.data[i];\n }\n\n if (includeOptions) {\n properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode);\n }\n\n if (!directUpdate) {\n this.updateElement(point, i, properties, mode);\n }\n\n prevParsed = parsed;\n }\n\n this.updateSharedOptions(sharedOptions, mode, firstOpts);\n }\n\n /**\n\t * @protected\n\t */\n getMaxOverflow() {\n const meta = this._cachedMeta;\n const data = meta.data || [];\n\n if (!this.options.showLine) {\n let max = 0;\n for (let i = data.length - 1; i >= 0; --i) {\n max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2);\n }\n return max > 0 && max;\n }\n\n const dataset = meta.dataset;\n const border = dataset.options && dataset.options.borderWidth || 0;\n\n if (!data.length) {\n return border;\n }\n\n const firstPoint = data[0].size(this.resolveDataElementOptions(0));\n const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1));\n return Math.max(border, firstPoint, lastPoint) / 2;\n }\n}\n","import Element from '../core/core.element.js';\nimport {_angleBetween, getAngleFromPoint, TAU, HALF_PI, valueOrDefault} from '../helpers/index.js';\nimport {PI, _isBetween, _limitValue} from '../helpers/helpers.math.js';\nimport {_readValueToProps} from '../helpers/helpers.options.js';\nimport type {ArcOptions, Point} from '../types/index.js';\n\n\nfunction clipArc(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) {\n const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element;\n let angleMargin = pixelMargin / outerRadius;\n\n // Draw an inner border by clipping the arc and drawing a double-width border\n // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders\n ctx.beginPath();\n ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin);\n if (innerRadius > pixelMargin) {\n angleMargin = pixelMargin / innerRadius;\n ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true);\n } else {\n ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI);\n }\n ctx.closePath();\n ctx.clip();\n}\n\nfunction toRadiusCorners(value) {\n return _readValueToProps(value, ['outerStart', 'outerEnd', 'innerStart', 'innerEnd']);\n}\n\n/**\n * Parse border radius from the provided options\n */\nfunction parseBorderRadius(arc: ArcElement, innerRadius: number, outerRadius: number, angleDelta: number) {\n const o = toRadiusCorners(arc.options.borderRadius);\n const halfThickness = (outerRadius - innerRadius) / 2;\n const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2);\n\n // Outer limits are complicated. We want to compute the available angular distance at\n // a radius of outerRadius - borderRadius because for small angular distances, this term limits.\n // We compute at r = outerRadius - borderRadius because this circle defines the center of the border corners.\n //\n // If the borderRadius is large, that value can become negative.\n // This causes the outer borders to lose their radius entirely, which is rather unexpected. To solve that, if borderRadius > outerRadius\n // we know that the thickness term will dominate and compute the limits at that point\n const computeOuterLimit = (val) => {\n const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2;\n return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit));\n };\n\n return {\n outerStart: computeOuterLimit(o.outerStart),\n outerEnd: computeOuterLimit(o.outerEnd),\n innerStart: _limitValue(o.innerStart, 0, innerLimit),\n innerEnd: _limitValue(o.innerEnd, 0, innerLimit),\n };\n}\n\n/**\n * Convert (r, 𝜃) to (x, y)\n */\nfunction rThetaToXY(r: number, theta: number, x: number, y: number) {\n return {\n x: x + r * Math.cos(theta),\n y: y + r * Math.sin(theta),\n };\n}\n\n\n/**\n * Path the arc, respecting border radius by separating into left and right halves.\n *\n * Start End\n *\n * 1--->a--->2 Outer\n * / \\\n * 8 3\n * | |\n * | |\n * 7 4\n * \\ /\n * 6<---b<---5 Inner\n */\nfunction pathArc(\n ctx: CanvasRenderingContext2D,\n element: ArcElement,\n offset: number,\n spacing: number,\n end: number,\n circular: boolean,\n) {\n const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element;\n\n const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0);\n const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0;\n\n let spacingOffset = 0;\n const alpha = end - start;\n\n if (spacing) {\n // When spacing is present, it is the same for all items\n // So we adjust the start and end angle of the arc such that\n // the distance is the same as it would be without the spacing\n const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0;\n const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0;\n const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2;\n const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha;\n spacingOffset = (alpha - adjustedAngle) / 2;\n }\n\n const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius;\n const angleOffset = (alpha - beta) / 2;\n const startAngle = start + angleOffset + spacingOffset;\n const endAngle = end - angleOffset - spacingOffset;\n const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius(element, innerRadius, outerRadius, endAngle - startAngle);\n\n const outerStartAdjustedRadius = outerRadius - outerStart;\n const outerEndAdjustedRadius = outerRadius - outerEnd;\n const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius;\n const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius;\n\n const innerStartAdjustedRadius = innerRadius + innerStart;\n const innerEndAdjustedRadius = innerRadius + innerEnd;\n const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius;\n const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius;\n\n ctx.beginPath();\n\n if (circular) {\n // The first arc segments from point 1 to point a to point 2\n const outerMidAdjustedAngle = (outerStartAdjustedAngle + outerEndAdjustedAngle) / 2;\n ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerMidAdjustedAngle);\n ctx.arc(x, y, outerRadius, outerMidAdjustedAngle, outerEndAdjustedAngle);\n\n // The corner segment from point 2 to point 3\n if (outerEnd > 0) {\n const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y);\n ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI);\n }\n\n // The line from point 3 to point 4\n const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y);\n ctx.lineTo(p4.x, p4.y);\n\n // The corner segment from point 4 to point 5\n if (innerEnd > 0) {\n const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y);\n ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI);\n }\n\n // The inner arc from point 5 to point b to point 6\n const innerMidAdjustedAngle = ((endAngle - (innerEnd / innerRadius)) + (startAngle + (innerStart / innerRadius))) / 2;\n ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), innerMidAdjustedAngle, true);\n ctx.arc(x, y, innerRadius, innerMidAdjustedAngle, startAngle + (innerStart / innerRadius), true);\n\n // The corner segment from point 6 to point 7\n if (innerStart > 0) {\n const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y);\n ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI);\n }\n\n // The line from point 7 to point 8\n const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y);\n ctx.lineTo(p8.x, p8.y);\n\n // The corner segment from point 8 to point 1\n if (outerStart > 0) {\n const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y);\n ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle);\n }\n } else {\n ctx.moveTo(x, y);\n\n const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x;\n const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y;\n ctx.lineTo(outerStartX, outerStartY);\n\n const outerEndX = Math.cos(outerEndAdjustedAngle) * outerRadius + x;\n const outerEndY = Math.sin(outerEndAdjustedAngle) * outerRadius + y;\n ctx.lineTo(outerEndX, outerEndY);\n }\n\n ctx.closePath();\n}\n\nfunction drawArc(\n ctx: CanvasRenderingContext2D,\n element: ArcElement,\n offset: number,\n spacing: number,\n circular: boolean,\n) {\n const {fullCircles, startAngle, circumference} = element;\n let endAngle = element.endAngle;\n if (fullCircles) {\n pathArc(ctx, element, offset, spacing, endAngle, circular);\n for (let i = 0; i < fullCircles; ++i) {\n ctx.fill();\n }\n if (!isNaN(circumference)) {\n endAngle = startAngle + (circumference % TAU || TAU);\n }\n }\n pathArc(ctx, element, offset, spacing, endAngle, circular);\n ctx.fill();\n return endAngle;\n}\n\nfunction drawBorder(\n ctx: CanvasRenderingContext2D,\n element: ArcElement,\n offset: number,\n spacing: number,\n circular: boolean,\n) {\n const {fullCircles, startAngle, circumference, options} = element;\n const {borderWidth, borderJoinStyle, borderDash, borderDashOffset} = options;\n const inner = options.borderAlign === 'inner';\n\n if (!borderWidth) {\n return;\n }\n\n ctx.setLineDash(borderDash || []);\n ctx.lineDashOffset = borderDashOffset;\n\n if (inner) {\n ctx.lineWidth = borderWidth * 2;\n ctx.lineJoin = borderJoinStyle || 'round';\n } else {\n ctx.lineWidth = borderWidth;\n ctx.lineJoin = borderJoinStyle || 'bevel';\n }\n\n let endAngle = element.endAngle;\n if (fullCircles) {\n pathArc(ctx, element, offset, spacing, endAngle, circular);\n for (let i = 0; i < fullCircles; ++i) {\n ctx.stroke();\n }\n if (!isNaN(circumference)) {\n endAngle = startAngle + (circumference % TAU || TAU);\n }\n }\n\n if (inner) {\n clipArc(ctx, element, endAngle);\n }\n\n if (!fullCircles) {\n pathArc(ctx, element, offset, spacing, endAngle, circular);\n ctx.stroke();\n }\n}\n\nexport interface ArcProps extends Point {\n startAngle: number;\n endAngle: number;\n innerRadius: number;\n outerRadius: number;\n circumference: number;\n}\n\nexport default class ArcElement extends Element {\n\n static id = 'arc';\n\n static defaults = {\n borderAlign: 'center',\n borderColor: '#fff',\n borderDash: [],\n borderDashOffset: 0,\n borderJoinStyle: undefined,\n borderRadius: 0,\n borderWidth: 2,\n offset: 0,\n spacing: 0,\n angle: undefined,\n circular: true,\n };\n\n static defaultRoutes = {\n backgroundColor: 'backgroundColor'\n };\n\n static descriptors = {\n _scriptable: true,\n _indexable: (name) => name !== 'borderDash'\n };\n\n circumference: number;\n endAngle: number;\n fullCircles: number;\n innerRadius: number;\n outerRadius: number;\n pixelMargin: number;\n startAngle: number;\n\n constructor(cfg) {\n super();\n\n this.options = undefined;\n this.circumference = undefined;\n this.startAngle = undefined;\n this.endAngle = undefined;\n this.innerRadius = undefined;\n this.outerRadius = undefined;\n this.pixelMargin = 0;\n this.fullCircles = 0;\n\n if (cfg) {\n Object.assign(this, cfg);\n }\n }\n\n inRange(chartX: number, chartY: number, useFinalPosition: boolean) {\n const point = this.getProps(['x', 'y'], useFinalPosition);\n const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY});\n const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([\n 'startAngle',\n 'endAngle',\n 'innerRadius',\n 'outerRadius',\n 'circumference'\n ], useFinalPosition);\n const rAdjust = (this.options.spacing + this.options.borderWidth) / 2;\n const _circumference = valueOrDefault(circumference, endAngle - startAngle);\n const nonZeroBetween = _angleBetween(angle, startAngle, endAngle) && startAngle !== endAngle;\n const betweenAngles = _circumference >= TAU || nonZeroBetween;\n const withinRadius = _isBetween(distance, innerRadius + rAdjust, outerRadius + rAdjust);\n\n return (betweenAngles && withinRadius);\n }\n\n getCenterPoint(useFinalPosition: boolean) {\n const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([\n 'x',\n 'y',\n 'startAngle',\n 'endAngle',\n 'innerRadius',\n 'outerRadius'\n ], useFinalPosition);\n const {offset, spacing} = this.options;\n const halfAngle = (startAngle + endAngle) / 2;\n const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2;\n return {\n x: x + Math.cos(halfAngle) * halfRadius,\n y: y + Math.sin(halfAngle) * halfRadius\n };\n }\n\n tooltipPosition(useFinalPosition: boolean) {\n return this.getCenterPoint(useFinalPosition);\n }\n\n draw(ctx: CanvasRenderingContext2D) {\n const {options, circumference} = this;\n const offset = (options.offset || 0) / 4;\n const spacing = (options.spacing || 0) / 2;\n const circular = options.circular;\n this.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;\n this.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0;\n\n if (circumference === 0 || this.innerRadius < 0 || this.outerRadius < 0) {\n return;\n }\n\n ctx.save();\n\n const halfAngle = (this.startAngle + this.endAngle) / 2;\n ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset);\n const fix = 1 - Math.sin(Math.min(PI, circumference || 0));\n const radiusOffset = offset * fix;\n\n ctx.fillStyle = options.backgroundColor;\n ctx.strokeStyle = options.borderColor;\n\n drawArc(ctx, this, radiusOffset, spacing, circular);\n drawBorder(ctx, this, radiusOffset, spacing, circular);\n\n ctx.restore();\n }\n}\n","import Element from '../core/core.element.js';\nimport {_bezierInterpolation, _pointInLine, _steppedInterpolation} from '../helpers/helpers.interpolation.js';\nimport {_computeSegments, _boundSegments} from '../helpers/helpers.segment.js';\nimport {_steppedLineTo, _bezierCurveTo} from '../helpers/helpers.canvas.js';\nimport {_updateBezierControlPoints} from '../helpers/helpers.curve.js';\nimport {valueOrDefault} from '../helpers/index.js';\n\n/**\n * @typedef { import('./element.point.js').default } PointElement\n */\n\nfunction setStyle(ctx, options, style = options) {\n ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle);\n ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash));\n ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset);\n ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle);\n ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth);\n ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor);\n}\n\nfunction lineTo(ctx, previous, target) {\n ctx.lineTo(target.x, target.y);\n}\n\n/**\n * @returns {any}\n */\nfunction getLineMethod(options) {\n if (options.stepped) {\n return _steppedLineTo;\n }\n\n if (options.tension || options.cubicInterpolationMode === 'monotone') {\n return _bezierCurveTo;\n }\n\n return lineTo;\n}\n\nfunction pathVars(points, segment, params = {}) {\n const count = points.length;\n const {start: paramsStart = 0, end: paramsEnd = count - 1} = params;\n const {start: segmentStart, end: segmentEnd} = segment;\n const start = Math.max(paramsStart, segmentStart);\n const end = Math.min(paramsEnd, segmentEnd);\n const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd;\n\n return {\n count,\n start,\n loop: segment.loop,\n ilen: end < start && !outside ? count + end - start : end - start\n };\n}\n\n/**\n * Create path from points, grouping by truncated x-coordinate\n * Points need to be in order by x-coordinate for this to work efficiently\n * @param {CanvasRenderingContext2D|Path2D} ctx - Context\n * @param {LineElement} line\n * @param {object} segment\n * @param {number} segment.start - start index of the segment, referring the points array\n * @param {number} segment.end - end index of the segment, referring the points array\n * @param {boolean} segment.loop - indicates that the segment is a loop\n * @param {object} params\n * @param {boolean} params.move - move to starting point (vs line to it)\n * @param {boolean} params.reverse - path the segment from end to start\n * @param {number} params.start - limit segment to points starting from `start` index\n * @param {number} params.end - limit segment to points ending at `start` + `count` index\n */\nfunction pathSegment(ctx, line, segment, params) {\n const {points, options} = line;\n const {count, start, loop, ilen} = pathVars(points, segment, params);\n const lineMethod = getLineMethod(options);\n // eslint-disable-next-line prefer-const\n let {move = true, reverse} = params || {};\n let i, point, prev;\n\n for (i = 0; i <= ilen; ++i) {\n point = points[(start + (reverse ? ilen - i : i)) % count];\n\n if (point.skip) {\n // If there is a skipped point inside a segment, spanGaps must be true\n continue;\n } else if (move) {\n ctx.moveTo(point.x, point.y);\n move = false;\n } else {\n lineMethod(ctx, prev, point, reverse, options.stepped);\n }\n\n prev = point;\n }\n\n if (loop) {\n point = points[(start + (reverse ? ilen : 0)) % count];\n lineMethod(ctx, prev, point, reverse, options.stepped);\n }\n\n return !!loop;\n}\n\n/**\n * Create path from points, grouping by truncated x-coordinate\n * Points need to be in order by x-coordinate for this to work efficiently\n * @param {CanvasRenderingContext2D|Path2D} ctx - Context\n * @param {LineElement} line\n * @param {object} segment\n * @param {number} segment.start - start index of the segment, referring the points array\n * @param {number} segment.end - end index of the segment, referring the points array\n * @param {boolean} segment.loop - indicates that the segment is a loop\n * @param {object} params\n * @param {boolean} params.move - move to starting point (vs line to it)\n * @param {boolean} params.reverse - path the segment from end to start\n * @param {number} params.start - limit segment to points starting from `start` index\n * @param {number} params.end - limit segment to points ending at `start` + `count` index\n */\nfunction fastPathSegment(ctx, line, segment, params) {\n const points = line.points;\n const {count, start, ilen} = pathVars(points, segment, params);\n const {move = true, reverse} = params || {};\n let avgX = 0;\n let countX = 0;\n let i, point, prevX, minY, maxY, lastY;\n\n const pointIndex = (index) => (start + (reverse ? ilen - index : index)) % count;\n const drawX = () => {\n if (minY !== maxY) {\n // Draw line to maxY and minY, using the average x-coordinate\n ctx.lineTo(avgX, maxY);\n ctx.lineTo(avgX, minY);\n // Line to y-value of last point in group. So the line continues\n // from correct position. Not using move, to have solid path.\n ctx.lineTo(avgX, lastY);\n }\n };\n\n if (move) {\n point = points[pointIndex(0)];\n ctx.moveTo(point.x, point.y);\n }\n\n for (i = 0; i <= ilen; ++i) {\n point = points[pointIndex(i)];\n\n if (point.skip) {\n // If there is a skipped point inside a segment, spanGaps must be true\n continue;\n }\n\n const x = point.x;\n const y = point.y;\n const truncX = x | 0; // truncated x-coordinate\n\n if (truncX === prevX) {\n // Determine `minY` / `maxY` and `avgX` while we stay within same x-position\n if (y < minY) {\n minY = y;\n } else if (y > maxY) {\n maxY = y;\n }\n // For first point in group, countX is `0`, so average will be `x` / 1.\n avgX = (countX * avgX + x) / ++countX;\n } else {\n drawX();\n // Draw line to next x-position, using the first (or only)\n // y-value in that group\n ctx.lineTo(x, y);\n\n prevX = truncX;\n countX = 0;\n minY = maxY = y;\n }\n // Keep track of the last y-value in group\n lastY = y;\n }\n drawX();\n}\n\n/**\n * @param {LineElement} line - the line\n * @returns {function}\n * @private\n */\nfunction _getSegmentMethod(line) {\n const opts = line.options;\n const borderDash = opts.borderDash && opts.borderDash.length;\n const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash;\n return useFastPath ? fastPathSegment : pathSegment;\n}\n\n/**\n * @private\n */\nfunction _getInterpolationMethod(options) {\n if (options.stepped) {\n return _steppedInterpolation;\n }\n\n if (options.tension || options.cubicInterpolationMode === 'monotone') {\n return _bezierInterpolation;\n }\n\n return _pointInLine;\n}\n\nfunction strokePathWithCache(ctx, line, start, count) {\n let path = line._path;\n if (!path) {\n path = line._path = new Path2D();\n if (line.path(path, start, count)) {\n path.closePath();\n }\n }\n setStyle(ctx, line.options);\n ctx.stroke(path);\n}\n\nfunction strokePathDirect(ctx, line, start, count) {\n const {segments, options} = line;\n const segmentMethod = _getSegmentMethod(line);\n\n for (const segment of segments) {\n setStyle(ctx, options, segment.style);\n ctx.beginPath();\n if (segmentMethod(ctx, line, segment, {start, end: start + count - 1})) {\n ctx.closePath();\n }\n ctx.stroke();\n }\n}\n\nconst usePath2D = typeof Path2D === 'function';\n\nfunction draw(ctx, line, start, count) {\n if (usePath2D && !line.options.segment) {\n strokePathWithCache(ctx, line, start, count);\n } else {\n strokePathDirect(ctx, line, start, count);\n }\n}\n\nexport default class LineElement extends Element {\n\n static id = 'line';\n\n /**\n * @type {any}\n */\n static defaults = {\n borderCapStyle: 'butt',\n borderDash: [],\n borderDashOffset: 0,\n borderJoinStyle: 'miter',\n borderWidth: 3,\n capBezierPoints: true,\n cubicInterpolationMode: 'default',\n fill: false,\n spanGaps: false,\n stepped: false,\n tension: 0,\n };\n\n /**\n * @type {any}\n */\n static defaultRoutes = {\n backgroundColor: 'backgroundColor',\n borderColor: 'borderColor'\n };\n\n\n static descriptors = {\n _scriptable: true,\n _indexable: (name) => name !== 'borderDash' && name !== 'fill',\n };\n\n\n constructor(cfg) {\n super();\n\n this.animated = true;\n this.options = undefined;\n this._chart = undefined;\n this._loop = undefined;\n this._fullLoop = undefined;\n this._path = undefined;\n this._points = undefined;\n this._segments = undefined;\n this._decimated = false;\n this._pointsUpdated = false;\n this._datasetIndex = undefined;\n\n if (cfg) {\n Object.assign(this, cfg);\n }\n }\n\n updateControlPoints(chartArea, indexAxis) {\n const options = this.options;\n if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !this._pointsUpdated) {\n const loop = options.spanGaps ? this._loop : this._fullLoop;\n _updateBezierControlPoints(this._points, options, chartArea, loop, indexAxis);\n this._pointsUpdated = true;\n }\n }\n\n set points(points) {\n this._points = points;\n delete this._segments;\n delete this._path;\n this._pointsUpdated = false;\n }\n\n get points() {\n return this._points;\n }\n\n get segments() {\n return this._segments || (this._segments = _computeSegments(this, this.options.segment));\n }\n\n /**\n\t * First non-skipped point on this line\n\t * @returns {PointElement|undefined}\n\t */\n first() {\n const segments = this.segments;\n const points = this.points;\n return segments.length && points[segments[0].start];\n }\n\n /**\n\t * Last non-skipped point on this line\n\t * @returns {PointElement|undefined}\n\t */\n last() {\n const segments = this.segments;\n const points = this.points;\n const count = segments.length;\n return count && points[segments[count - 1].end];\n }\n\n /**\n\t * Interpolate a point in this line at the same value on `property` as\n\t * the reference `point` provided\n\t * @param {PointElement} point - the reference point\n\t * @param {string} property - the property to match on\n\t * @returns {PointElement|undefined}\n\t */\n interpolate(point, property) {\n const options = this.options;\n const value = point[property];\n const points = this.points;\n const segments = _boundSegments(this, {property, start: value, end: value});\n\n if (!segments.length) {\n return;\n }\n\n const result = [];\n const _interpolate = _getInterpolationMethod(options);\n let i, ilen;\n for (i = 0, ilen = segments.length; i < ilen; ++i) {\n const {start, end} = segments[i];\n const p1 = points[start];\n const p2 = points[end];\n if (p1 === p2) {\n result.push(p1);\n continue;\n }\n const t = Math.abs((value - p1[property]) / (p2[property] - p1[property]));\n const interpolated = _interpolate(p1, p2, t, options.stepped);\n interpolated[property] = point[property];\n result.push(interpolated);\n }\n return result.length === 1 ? result[0] : result;\n }\n\n /**\n\t * Append a segment of this line to current path.\n\t * @param {CanvasRenderingContext2D} ctx\n\t * @param {object} segment\n\t * @param {number} segment.start - start index of the segment, referring the points array\n \t * @param {number} segment.end - end index of the segment, referring the points array\n \t * @param {boolean} segment.loop - indicates that the segment is a loop\n\t * @param {object} params\n\t * @param {boolean} params.move - move to starting point (vs line to it)\n\t * @param {boolean} params.reverse - path the segment from end to start\n\t * @param {number} params.start - limit segment to points starting from `start` index\n\t * @param {number} params.end - limit segment to points ending at `start` + `count` index\n\t * @returns {undefined|boolean} - true if the segment is a full loop (path should be closed)\n\t */\n pathSegment(ctx, segment, params) {\n const segmentMethod = _getSegmentMethod(this);\n return segmentMethod(ctx, this, segment, params);\n }\n\n /**\n\t * Append all segments of this line to current path.\n\t * @param {CanvasRenderingContext2D|Path2D} ctx\n\t * @param {number} [start]\n\t * @param {number} [count]\n\t * @returns {undefined|boolean} - true if line is a full loop (path should be closed)\n\t */\n path(ctx, start, count) {\n const segments = this.segments;\n const segmentMethod = _getSegmentMethod(this);\n let loop = this._loop;\n\n start = start || 0;\n count = count || (this.points.length - start);\n\n for (const segment of segments) {\n loop &= segmentMethod(ctx, this, segment, {start, end: start + count - 1});\n }\n return !!loop;\n }\n\n /**\n\t * Draw\n\t * @param {CanvasRenderingContext2D} ctx\n\t * @param {object} chartArea\n\t * @param {number} [start]\n\t * @param {number} [count]\n\t */\n draw(ctx, chartArea, start, count) {\n const options = this.options || {};\n const points = this.points || [];\n\n if (points.length && options.borderWidth) {\n ctx.save();\n\n draw(ctx, this, start, count);\n\n ctx.restore();\n }\n\n if (this.animated) {\n // When line is animated, the control points and path are not cached.\n this._pointsUpdated = false;\n this._path = undefined;\n }\n }\n}\n","import Element from '../core/core.element.js';\nimport {drawPoint, _isPointInArea} from '../helpers/helpers.canvas.js';\nimport type {\n CartesianParsedData,\n ChartArea,\n Point,\n PointHoverOptions,\n PointOptions,\n} from '../types/index.js';\n\nfunction inRange(el: PointElement, pos: number, axis: 'x' | 'y', useFinalPosition?: boolean) {\n const options = el.options;\n const {[axis]: value} = el.getProps([axis], useFinalPosition);\n\n return (Math.abs(pos - value) < options.radius + options.hitRadius);\n}\n\nexport type PointProps = Point\n\nexport default class PointElement extends Element {\n\n static id = 'point';\n\n parsed: CartesianParsedData;\n skip?: boolean;\n stop?: boolean;\n\n /**\n * @type {any}\n */\n static defaults = {\n borderWidth: 1,\n hitRadius: 1,\n hoverBorderWidth: 1,\n hoverRadius: 4,\n pointStyle: 'circle',\n radius: 3,\n rotation: 0\n };\n\n /**\n * @type {any}\n */\n static defaultRoutes = {\n backgroundColor: 'backgroundColor',\n borderColor: 'borderColor'\n };\n\n constructor(cfg) {\n super();\n\n this.options = undefined;\n this.parsed = undefined;\n this.skip = undefined;\n this.stop = undefined;\n\n if (cfg) {\n Object.assign(this, cfg);\n }\n }\n\n inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean) {\n const options = this.options;\n const {x, y} = this.getProps(['x', 'y'], useFinalPosition);\n return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2));\n }\n\n inXRange(mouseX: number, useFinalPosition?: boolean) {\n return inRange(this, mouseX, 'x', useFinalPosition);\n }\n\n inYRange(mouseY: number, useFinalPosition?: boolean) {\n return inRange(this, mouseY, 'y', useFinalPosition);\n }\n\n getCenterPoint(useFinalPosition?: boolean) {\n const {x, y} = this.getProps(['x', 'y'], useFinalPosition);\n return {x, y};\n }\n\n size(options?: Partial) {\n options = options || this.options || {};\n let radius = options.radius || 0;\n radius = Math.max(radius, radius && options.hoverRadius || 0);\n const borderWidth = radius && options.borderWidth || 0;\n return (radius + borderWidth) * 2;\n }\n\n draw(ctx: CanvasRenderingContext2D, area: ChartArea) {\n const options = this.options;\n\n if (this.skip || options.radius < 0.1 || !_isPointInArea(this, area, this.size(options) / 2)) {\n return;\n }\n\n ctx.strokeStyle = options.borderColor;\n ctx.lineWidth = options.borderWidth;\n ctx.fillStyle = options.backgroundColor;\n drawPoint(ctx, options, this.x, this.y);\n }\n\n getRange() {\n const options = this.options || {};\n // @ts-expect-error Fallbacks should never be hit in practice\n return options.radius + options.hitRadius;\n }\n}\n","import Element from '../core/core.element.js';\nimport {isObject, _isBetween, _limitValue} from '../helpers/index.js';\nimport {addRoundedRectPath} from '../helpers/helpers.canvas.js';\nimport {toTRBL, toTRBLCorners} from '../helpers/helpers.options.js';\n\n/** @typedef {{ x: number, y: number, base: number, horizontal: boolean, width: number, height: number }} BarProps */\n\n/**\n * Helper function to get the bounds of the bar regardless of the orientation\n * @param {BarElement} bar the bar\n * @param {boolean} [useFinalPosition]\n * @return {object} bounds of the bar\n * @private\n */\nfunction getBarBounds(bar, useFinalPosition) {\n const {x, y, base, width, height} = /** @type {BarProps} */ (bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition));\n\n let left, right, top, bottom, half;\n\n if (bar.horizontal) {\n half = height / 2;\n left = Math.min(x, base);\n right = Math.max(x, base);\n top = y - half;\n bottom = y + half;\n } else {\n half = width / 2;\n left = x - half;\n right = x + half;\n top = Math.min(y, base);\n bottom = Math.max(y, base);\n }\n\n return {left, top, right, bottom};\n}\n\nfunction skipOrLimit(skip, value, min, max) {\n return skip ? 0 : _limitValue(value, min, max);\n}\n\nfunction parseBorderWidth(bar, maxW, maxH) {\n const value = bar.options.borderWidth;\n const skip = bar.borderSkipped;\n const o = toTRBL(value);\n\n return {\n t: skipOrLimit(skip.top, o.top, 0, maxH),\n r: skipOrLimit(skip.right, o.right, 0, maxW),\n b: skipOrLimit(skip.bottom, o.bottom, 0, maxH),\n l: skipOrLimit(skip.left, o.left, 0, maxW)\n };\n}\n\nfunction parseBorderRadius(bar, maxW, maxH) {\n const {enableBorderRadius} = bar.getProps(['enableBorderRadius']);\n const value = bar.options.borderRadius;\n const o = toTRBLCorners(value);\n const maxR = Math.min(maxW, maxH);\n const skip = bar.borderSkipped;\n\n // If the value is an object, assume the user knows what they are doing\n // and apply as directed.\n const enableBorder = enableBorderRadius || isObject(value);\n\n return {\n topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR),\n topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR),\n bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR),\n bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR)\n };\n}\n\nfunction boundingRects(bar) {\n const bounds = getBarBounds(bar);\n const width = bounds.right - bounds.left;\n const height = bounds.bottom - bounds.top;\n const border = parseBorderWidth(bar, width / 2, height / 2);\n const radius = parseBorderRadius(bar, width / 2, height / 2);\n\n return {\n outer: {\n x: bounds.left,\n y: bounds.top,\n w: width,\n h: height,\n radius\n },\n inner: {\n x: bounds.left + border.l,\n y: bounds.top + border.t,\n w: width - border.l - border.r,\n h: height - border.t - border.b,\n radius: {\n topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),\n topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),\n bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),\n bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),\n }\n }\n };\n}\n\nfunction inRange(bar, x, y, useFinalPosition) {\n const skipX = x === null;\n const skipY = y === null;\n const skipBoth = skipX && skipY;\n const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition);\n\n return bounds\n\t\t&& (skipX || _isBetween(x, bounds.left, bounds.right))\n\t\t&& (skipY || _isBetween(y, bounds.top, bounds.bottom));\n}\n\nfunction hasRadius(radius) {\n return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;\n}\n\n/**\n * Add a path of a rectangle to the current sub-path\n * @param {CanvasRenderingContext2D} ctx Context\n * @param {*} rect Bounding rect\n */\nfunction addNormalRectPath(ctx, rect) {\n ctx.rect(rect.x, rect.y, rect.w, rect.h);\n}\n\nfunction inflateRect(rect, amount, refRect = {}) {\n const x = rect.x !== refRect.x ? -amount : 0;\n const y = rect.y !== refRect.y ? -amount : 0;\n const w = (rect.x + rect.w !== refRect.x + refRect.w ? amount : 0) - x;\n const h = (rect.y + rect.h !== refRect.y + refRect.h ? amount : 0) - y;\n return {\n x: rect.x + x,\n y: rect.y + y,\n w: rect.w + w,\n h: rect.h + h,\n radius: rect.radius\n };\n}\n\nexport default class BarElement extends Element {\n\n static id = 'bar';\n\n /**\n * @type {any}\n */\n static defaults = {\n borderSkipped: 'start',\n borderWidth: 0,\n borderRadius: 0,\n inflateAmount: 'auto',\n pointStyle: undefined\n };\n\n /**\n * @type {any}\n */\n static defaultRoutes = {\n backgroundColor: 'backgroundColor',\n borderColor: 'borderColor'\n };\n\n constructor(cfg) {\n super();\n\n this.options = undefined;\n this.horizontal = undefined;\n this.base = undefined;\n this.width = undefined;\n this.height = undefined;\n this.inflateAmount = undefined;\n\n if (cfg) {\n Object.assign(this, cfg);\n }\n }\n\n draw(ctx) {\n const {inflateAmount, options: {borderColor, backgroundColor}} = this;\n const {inner, outer} = boundingRects(this);\n const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;\n\n ctx.save();\n\n if (outer.w !== inner.w || outer.h !== inner.h) {\n ctx.beginPath();\n addRectPath(ctx, inflateRect(outer, inflateAmount, inner));\n ctx.clip();\n addRectPath(ctx, inflateRect(inner, -inflateAmount, outer));\n ctx.fillStyle = borderColor;\n ctx.fill('evenodd');\n }\n\n ctx.beginPath();\n addRectPath(ctx, inflateRect(inner, inflateAmount));\n ctx.fillStyle = backgroundColor;\n ctx.fill();\n\n ctx.restore();\n }\n\n inRange(mouseX, mouseY, useFinalPosition) {\n return inRange(this, mouseX, mouseY, useFinalPosition);\n }\n\n inXRange(mouseX, useFinalPosition) {\n return inRange(this, mouseX, null, useFinalPosition);\n }\n\n inYRange(mouseY, useFinalPosition) {\n return inRange(this, null, mouseY, useFinalPosition);\n }\n\n getCenterPoint(useFinalPosition) {\n const {x, y, base, horizontal} = /** @type {BarProps} */ (this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition));\n return {\n x: horizontal ? (x + base) / 2 : x,\n y: horizontal ? y : (y + base) / 2\n };\n }\n\n getRange(axis) {\n return axis === 'x' ? this.width / 2 : this.height / 2;\n }\n}\n","import Scale from '../core/core.scale.js';\nimport {isNullOrUndef, valueOrDefault, _limitValue} from '../helpers/index.js';\n\nconst addIfString = (labels, raw, index, addedLabels) => {\n if (typeof raw === 'string') {\n index = labels.push(raw) - 1;\n addedLabels.unshift({index, label: raw});\n } else if (isNaN(raw)) {\n index = null;\n }\n return index;\n};\n\nfunction findOrAddLabel(labels, raw, index, addedLabels) {\n const first = labels.indexOf(raw);\n if (first === -1) {\n return addIfString(labels, raw, index, addedLabels);\n }\n const last = labels.lastIndexOf(raw);\n return first !== last ? index : first;\n}\n\nconst validIndex = (index, max) => index === null ? null : _limitValue(Math.round(index), 0, max);\n\nfunction _getLabelForValue(value) {\n const labels = this.getLabels();\n\n if (value >= 0 && value < labels.length) {\n return labels[value];\n }\n return value;\n}\n\nexport default class CategoryScale extends Scale {\n\n static id = 'category';\n\n /**\n * @type {any}\n */\n static defaults = {\n ticks: {\n callback: _getLabelForValue\n }\n };\n\n constructor(cfg) {\n super(cfg);\n\n /** @type {number} */\n this._startValue = undefined;\n this._valueRange = 0;\n this._addedLabels = [];\n }\n\n init(scaleOptions) {\n const added = this._addedLabels;\n if (added.length) {\n const labels = this.getLabels();\n for (const {index, label} of added) {\n if (labels[index] === label) {\n labels.splice(index, 1);\n }\n }\n this._addedLabels = [];\n }\n super.init(scaleOptions);\n }\n\n parse(raw, index) {\n if (isNullOrUndef(raw)) {\n return null;\n }\n const labels = this.getLabels();\n index = isFinite(index) && labels[index] === raw ? index\n : findOrAddLabel(labels, raw, valueOrDefault(index, raw), this._addedLabels);\n return validIndex(index, labels.length - 1);\n }\n\n determineDataLimits() {\n const {minDefined, maxDefined} = this.getUserBounds();\n let {min, max} = this.getMinMax(true);\n\n if (this.options.bounds === 'ticks') {\n if (!minDefined) {\n min = 0;\n }\n if (!maxDefined) {\n max = this.getLabels().length - 1;\n }\n }\n\n this.min = min;\n this.max = max;\n }\n\n buildTicks() {\n const min = this.min;\n const max = this.max;\n const offset = this.options.offset;\n const ticks = [];\n let labels = this.getLabels();\n\n // If we are viewing some subset of labels, slice the original array\n labels = (min === 0 && max === labels.length - 1) ? labels : labels.slice(min, max + 1);\n\n this._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1);\n this._startValue = this.min - (offset ? 0.5 : 0);\n\n for (let value = min; value <= max; value++) {\n ticks.push({value});\n }\n return ticks;\n }\n\n getLabelForValue(value) {\n return _getLabelForValue.call(this, value);\n }\n\n /**\n\t * @protected\n\t */\n configure() {\n super.configure();\n\n if (!this.isHorizontal()) {\n // For backward compatibility, vertical category scale reverse is inverted.\n this._reversePixels = !this._reversePixels;\n }\n }\n\n // Used to get data value locations. Value can either be an index or a numerical value\n getPixelForValue(value) {\n if (typeof value !== 'number') {\n value = this.parse(value);\n }\n\n return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange);\n }\n\n // Must override base implementation because it calls getPixelForValue\n // and category scale can have duplicate values\n getPixelForTick(index) {\n const ticks = this.ticks;\n if (index < 0 || index > ticks.length - 1) {\n return null;\n }\n return this.getPixelForValue(ticks[index].value);\n }\n\n getValueForPixel(pixel) {\n return Math.round(this._startValue + this.getDecimalForPixel(pixel) * this._valueRange);\n }\n\n getBasePixel() {\n return this.bottom;\n }\n}\n","import {isNullOrUndef} from '../helpers/helpers.core.js';\nimport {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign, toRadians} from '../helpers/helpers.math.js';\nimport Scale from '../core/core.scale.js';\nimport {formatNumber} from '../helpers/helpers.intl.js';\n\n/**\n * Generate a set of linear ticks for an axis\n * 1. If generationOptions.min, generationOptions.max, and generationOptions.step are defined:\n * if (max - min) / step is an integer, ticks are generated as [min, min + step, ..., max]\n * Note that the generationOptions.maxCount setting is respected in this scenario\n *\n * 2. If generationOptions.min, generationOptions.max, and generationOptions.count is defined\n * spacing = (max - min) / count\n * Ticks are generated as [min, min + spacing, ..., max]\n *\n * 3. If generationOptions.count is defined\n * spacing = (niceMax - niceMin) / count\n *\n * 4. Compute optimal spacing of ticks using niceNum algorithm\n *\n * @param generationOptions the options used to generate the ticks\n * @param dataRange the range of the data\n * @returns {object[]} array of tick objects\n */\nfunction generateTicks(generationOptions, dataRange) {\n const ticks = [];\n // To get a \"nice\" value for the tick spacing, we will use the appropriately named\n // \"nice number\" algorithm. See https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks\n // for details.\n\n const MIN_SPACING = 1e-14;\n const {bounds, step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions;\n const unit = step || 1;\n const maxSpaces = maxTicks - 1;\n const {min: rmin, max: rmax} = dataRange;\n const minDefined = !isNullOrUndef(min);\n const maxDefined = !isNullOrUndef(max);\n const countDefined = !isNullOrUndef(count);\n const minSpacing = (rmax - rmin) / (maxDigits + 1);\n let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit;\n let factor, niceMin, niceMax, numSpaces;\n\n // Beyond MIN_SPACING floating point numbers being to lose precision\n // such that we can't do the math necessary to generate ticks\n if (spacing < MIN_SPACING && !minDefined && !maxDefined) {\n return [{value: rmin}, {value: rmax}];\n }\n\n numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing);\n if (numSpaces > maxSpaces) {\n // If the calculated num of spaces exceeds maxNumSpaces, recalculate it\n spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit;\n }\n\n if (!isNullOrUndef(precision)) {\n // If the user specified a precision, round to that number of decimal places\n factor = Math.pow(10, precision);\n spacing = Math.ceil(spacing * factor) / factor;\n }\n\n if (bounds === 'ticks') {\n niceMin = Math.floor(rmin / spacing) * spacing;\n niceMax = Math.ceil(rmax / spacing) * spacing;\n } else {\n niceMin = rmin;\n niceMax = rmax;\n }\n\n if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) {\n // Case 1: If min, max and stepSize are set and they make an evenly spaced scale use it.\n // spacing = step;\n // numSpaces = (max - min) / spacing;\n // Note that we round here to handle the case where almostWhole translated an FP error\n numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks));\n spacing = (max - min) / numSpaces;\n niceMin = min;\n niceMax = max;\n } else if (countDefined) {\n // Cases 2 & 3, we have a count specified. Handle optional user defined edges to the range.\n // Sometimes these are no-ops, but it makes the code a lot clearer\n // and when a user defined range is specified, we want the correct ticks\n niceMin = minDefined ? min : niceMin;\n niceMax = maxDefined ? max : niceMax;\n numSpaces = count - 1;\n spacing = (niceMax - niceMin) / numSpaces;\n } else {\n // Case 4\n numSpaces = (niceMax - niceMin) / spacing;\n\n // If very close to our rounded value, use it.\n if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {\n numSpaces = Math.round(numSpaces);\n } else {\n numSpaces = Math.ceil(numSpaces);\n }\n }\n\n // The spacing will have changed in cases 1, 2, and 3 so the factor cannot be computed\n // until this point\n const decimalPlaces = Math.max(\n _decimalPlaces(spacing),\n _decimalPlaces(niceMin)\n );\n factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision);\n niceMin = Math.round(niceMin * factor) / factor;\n niceMax = Math.round(niceMax * factor) / factor;\n\n let j = 0;\n if (minDefined) {\n if (includeBounds && niceMin !== min) {\n ticks.push({value: min});\n\n if (niceMin < min) {\n j++; // Skip niceMin\n }\n // If the next nice tick is close to min, skip it\n if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) {\n j++;\n }\n } else if (niceMin < min) {\n j++;\n }\n }\n\n for (; j < numSpaces; ++j) {\n const tickValue = Math.round((niceMin + j * spacing) * factor) / factor;\n if (maxDefined && tickValue > max) {\n break;\n }\n ticks.push({value: tickValue});\n }\n\n if (maxDefined && includeBounds && niceMax !== max) {\n // If the previous tick is too close to max, replace it with max, else add max\n if (ticks.length && almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) {\n ticks[ticks.length - 1].value = max;\n } else {\n ticks.push({value: max});\n }\n } else if (!maxDefined || niceMax === max) {\n ticks.push({value: niceMax});\n }\n\n return ticks;\n}\n\nfunction relativeLabelSize(value, minSpacing, {horizontal, minRotation}) {\n const rad = toRadians(minRotation);\n const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001;\n const length = 0.75 * minSpacing * ('' + value).length;\n return Math.min(minSpacing / ratio, length);\n}\n\nexport default class LinearScaleBase extends Scale {\n\n constructor(cfg) {\n super(cfg);\n\n /** @type {number} */\n this.start = undefined;\n /** @type {number} */\n this.end = undefined;\n /** @type {number} */\n this._startValue = undefined;\n /** @type {number} */\n this._endValue = undefined;\n this._valueRange = 0;\n }\n\n parse(raw, index) { // eslint-disable-line no-unused-vars\n if (isNullOrUndef(raw)) {\n return null;\n }\n if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) {\n return null;\n }\n\n return +raw;\n }\n\n handleTickRangeOptions() {\n const {beginAtZero} = this.options;\n const {minDefined, maxDefined} = this.getUserBounds();\n let {min, max} = this;\n\n const setMin = v => (min = minDefined ? min : v);\n const setMax = v => (max = maxDefined ? max : v);\n\n if (beginAtZero) {\n const minSign = sign(min);\n const maxSign = sign(max);\n\n if (minSign < 0 && maxSign < 0) {\n setMax(0);\n } else if (minSign > 0 && maxSign > 0) {\n setMin(0);\n }\n }\n\n if (min === max) {\n let offset = max === 0 ? 1 : Math.abs(max * 0.05);\n\n setMax(max + offset);\n\n if (!beginAtZero) {\n setMin(min - offset);\n }\n }\n this.min = min;\n this.max = max;\n }\n\n getTickLimit() {\n const tickOpts = this.options.ticks;\n // eslint-disable-next-line prefer-const\n let {maxTicksLimit, stepSize} = tickOpts;\n let maxTicks;\n\n if (stepSize) {\n maxTicks = Math.ceil(this.max / stepSize) - Math.floor(this.min / stepSize) + 1;\n if (maxTicks > 1000) {\n console.warn(`scales.${this.id}.ticks.stepSize: ${stepSize} would result generating up to ${maxTicks} ticks. Limiting to 1000.`);\n maxTicks = 1000;\n }\n } else {\n maxTicks = this.computeTickLimit();\n maxTicksLimit = maxTicksLimit || 11;\n }\n\n if (maxTicksLimit) {\n maxTicks = Math.min(maxTicksLimit, maxTicks);\n }\n\n return maxTicks;\n }\n\n /**\n\t * @protected\n\t */\n computeTickLimit() {\n return Number.POSITIVE_INFINITY;\n }\n\n buildTicks() {\n const opts = this.options;\n const tickOpts = opts.ticks;\n\n // Figure out what the max number of ticks we can support it is based on the size of\n // the axis area. For now, we say that the minimum tick spacing in pixels must be 40\n // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on\n // the graph. Make sure we always have at least 2 ticks\n let maxTicks = this.getTickLimit();\n maxTicks = Math.max(2, maxTicks);\n\n const numericGeneratorOptions = {\n maxTicks,\n bounds: opts.bounds,\n min: opts.min,\n max: opts.max,\n precision: tickOpts.precision,\n step: tickOpts.stepSize,\n count: tickOpts.count,\n maxDigits: this._maxDigits(),\n horizontal: this.isHorizontal(),\n minRotation: tickOpts.minRotation || 0,\n includeBounds: tickOpts.includeBounds !== false\n };\n const dataRange = this._range || this;\n const ticks = generateTicks(numericGeneratorOptions, dataRange);\n\n // At this point, we need to update our max and min given the tick values,\n // since we probably have expanded the range of the scale\n if (opts.bounds === 'ticks') {\n _setMinAndMaxByKey(ticks, this, 'value');\n }\n\n if (opts.reverse) {\n ticks.reverse();\n\n this.start = this.max;\n this.end = this.min;\n } else {\n this.start = this.min;\n this.end = this.max;\n }\n\n return ticks;\n }\n\n /**\n\t * @protected\n\t */\n configure() {\n const ticks = this.ticks;\n let start = this.min;\n let end = this.max;\n\n super.configure();\n\n if (this.options.offset && ticks.length) {\n const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2;\n start -= offset;\n end += offset;\n }\n this._startValue = start;\n this._endValue = end;\n this._valueRange = end - start;\n }\n\n getLabelForValue(value) {\n return formatNumber(value, this.chart.options.locale, this.options.ticks.format);\n }\n}\n","import {isFinite} from '../helpers/helpers.core.js';\nimport LinearScaleBase from './scale.linearbase.js';\nimport Ticks from '../core/core.ticks.js';\nimport {toRadians} from '../helpers/index.js';\n\nexport default class LinearScale extends LinearScaleBase {\n\n static id = 'linear';\n\n /**\n * @type {any}\n */\n static defaults = {\n ticks: {\n callback: Ticks.formatters.numeric\n }\n };\n\n\n determineDataLimits() {\n const {min, max} = this.getMinMax(true);\n\n this.min = isFinite(min) ? min : 0;\n this.max = isFinite(max) ? max : 1;\n\n // Common base implementation to handle min, max, beginAtZero\n this.handleTickRangeOptions();\n }\n\n /**\n\t * Returns the maximum number of ticks based on the scale dimension\n\t * @protected\n \t */\n computeTickLimit() {\n const horizontal = this.isHorizontal();\n const length = horizontal ? this.width : this.height;\n const minRotation = toRadians(this.options.ticks.minRotation);\n const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001;\n const tickFont = this._resolveTickFontOptions(0);\n return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio));\n }\n\n // Utils\n getPixelForValue(value) {\n return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange);\n }\n\n getValueForPixel(pixel) {\n return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange;\n }\n}\n","import {finiteOrDefault, isFinite} from '../helpers/helpers.core.js';\nimport {formatNumber} from '../helpers/helpers.intl.js';\nimport {_setMinAndMaxByKey, log10} from '../helpers/helpers.math.js';\nimport Scale from '../core/core.scale.js';\nimport LinearScaleBase from './scale.linearbase.js';\nimport Ticks from '../core/core.ticks.js';\n\nconst log10Floor = v => Math.floor(log10(v));\nconst changeExponent = (v, m) => Math.pow(10, log10Floor(v) + m);\n\nfunction isMajor(tickVal) {\n const remain = tickVal / (Math.pow(10, log10Floor(tickVal)));\n return remain === 1;\n}\n\nfunction steps(min, max, rangeExp) {\n const rangeStep = Math.pow(10, rangeExp);\n const start = Math.floor(min / rangeStep);\n const end = Math.ceil(max / rangeStep);\n return end - start;\n}\n\nfunction startExp(min, max) {\n const range = max - min;\n let rangeExp = log10Floor(range);\n while (steps(min, max, rangeExp) > 10) {\n rangeExp++;\n }\n while (steps(min, max, rangeExp) < 10) {\n rangeExp--;\n }\n return Math.min(rangeExp, log10Floor(min));\n}\n\n\n/**\n * Generate a set of logarithmic ticks\n * @param generationOptions the options used to generate the ticks\n * @param dataRange the range of the data\n * @returns {object[]} array of tick objects\n */\nfunction generateTicks(generationOptions, {min, max}) {\n min = finiteOrDefault(generationOptions.min, min);\n const ticks = [];\n const minExp = log10Floor(min);\n let exp = startExp(min, max);\n let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;\n const stepSize = Math.pow(10, exp);\n const base = minExp > exp ? Math.pow(10, minExp) : 0;\n const start = Math.round((min - base) * precision) / precision;\n const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10;\n let significand = Math.floor((start - offset) / Math.pow(10, exp));\n let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision);\n while (value < max) {\n ticks.push({value, major: isMajor(value), significand});\n if (significand >= 10) {\n significand = significand < 15 ? 15 : 20;\n } else {\n significand++;\n }\n if (significand >= 20) {\n exp++;\n significand = 2;\n precision = exp >= 0 ? 1 : precision;\n }\n value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision;\n }\n const lastTick = finiteOrDefault(generationOptions.max, value);\n ticks.push({value: lastTick, major: isMajor(lastTick), significand});\n\n return ticks;\n}\n\nexport default class LogarithmicScale extends Scale {\n\n static id = 'logarithmic';\n\n /**\n * @type {any}\n */\n static defaults = {\n ticks: {\n callback: Ticks.formatters.logarithmic,\n major: {\n enabled: true\n }\n }\n };\n\n\n constructor(cfg) {\n super(cfg);\n\n /** @type {number} */\n this.start = undefined;\n /** @type {number} */\n this.end = undefined;\n /** @type {number} */\n this._startValue = undefined;\n this._valueRange = 0;\n }\n\n parse(raw, index) {\n const value = LinearScaleBase.prototype.parse.apply(this, [raw, index]);\n if (value === 0) {\n this._zero = true;\n return undefined;\n }\n return isFinite(value) && value > 0 ? value : null;\n }\n\n determineDataLimits() {\n const {min, max} = this.getMinMax(true);\n\n this.min = isFinite(min) ? Math.max(0, min) : null;\n this.max = isFinite(max) ? Math.max(0, max) : null;\n\n if (this.options.beginAtZero) {\n this._zero = true;\n }\n\n // if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom\n // of scale, and it does not equal suggestedMin, lower the min bound by one exp.\n if (this._zero && this.min !== this._suggestedMin && !isFinite(this._userMin)) {\n this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0);\n }\n\n this.handleTickRangeOptions();\n }\n\n handleTickRangeOptions() {\n const {minDefined, maxDefined} = this.getUserBounds();\n let min = this.min;\n let max = this.max;\n\n const setMin = v => (min = minDefined ? min : v);\n const setMax = v => (max = maxDefined ? max : v);\n\n if (min === max) {\n if (min <= 0) { // includes null\n setMin(1);\n setMax(10);\n } else {\n setMin(changeExponent(min, -1));\n setMax(changeExponent(max, +1));\n }\n }\n if (min <= 0) {\n setMin(changeExponent(max, -1));\n }\n if (max <= 0) {\n\n setMax(changeExponent(min, +1));\n }\n\n this.min = min;\n this.max = max;\n }\n\n buildTicks() {\n const opts = this.options;\n\n const generationOptions = {\n min: this._userMin,\n max: this._userMax\n };\n const ticks = generateTicks(generationOptions, this);\n\n // At this point, we need to update our max and min given the tick values,\n // since we probably have expanded the range of the scale\n if (opts.bounds === 'ticks') {\n _setMinAndMaxByKey(ticks, this, 'value');\n }\n\n if (opts.reverse) {\n ticks.reverse();\n\n this.start = this.max;\n this.end = this.min;\n } else {\n this.start = this.min;\n this.end = this.max;\n }\n\n return ticks;\n }\n\n /**\n\t * @param {number} value\n\t * @return {string}\n\t */\n getLabelForValue(value) {\n return value === undefined\n ? '0'\n : formatNumber(value, this.chart.options.locale, this.options.ticks.format);\n }\n\n /**\n\t * @protected\n\t */\n configure() {\n const start = this.min;\n\n super.configure();\n\n this._startValue = log10(start);\n this._valueRange = log10(this.max) - log10(start);\n }\n\n getPixelForValue(value) {\n if (value === undefined || value === 0) {\n value = this.min;\n }\n if (value === null || isNaN(value)) {\n return NaN;\n }\n return this.getPixelForDecimal(value === this.min\n ? 0\n : (log10(value) - this._startValue) / this._valueRange);\n }\n\n getValueForPixel(pixel) {\n const decimal = this.getDecimalForPixel(pixel);\n return Math.pow(10, this._startValue + decimal * this._valueRange);\n }\n}\n","import defaults from '../core/core.defaults.js';\nimport {_longestText, addRoundedRectPath, renderText, _isPointInArea} from '../helpers/helpers.canvas.js';\nimport {HALF_PI, TAU, toDegrees, toRadians, _normalizeAngle, PI} from '../helpers/helpers.math.js';\nimport LinearScaleBase from './scale.linearbase.js';\nimport Ticks from '../core/core.ticks.js';\nimport {valueOrDefault, isArray, isFinite, callback as callCallback, isNullOrUndef} from '../helpers/helpers.core.js';\nimport {createContext, toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js';\n\nfunction getTickBackdropHeight(opts) {\n const tickOpts = opts.ticks;\n\n if (tickOpts.display && opts.display) {\n const padding = toPadding(tickOpts.backdropPadding);\n return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height;\n }\n return 0;\n}\n\nfunction measureLabelSize(ctx, font, label) {\n label = isArray(label) ? label : [label];\n return {\n w: _longestText(ctx, font.string, label),\n h: label.length * font.lineHeight\n };\n}\n\nfunction determineLimits(angle, pos, size, min, max) {\n if (angle === min || angle === max) {\n return {\n start: pos - (size / 2),\n end: pos + (size / 2)\n };\n } else if (angle < min || angle > max) {\n return {\n start: pos - size,\n end: pos\n };\n }\n\n return {\n start: pos,\n end: pos + size\n };\n}\n\n/**\n * Helper function to fit a radial linear scale with point labels\n */\nfunction fitWithPointLabels(scale) {\n\n // Right, this is really confusing and there is a lot of maths going on here\n // The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9\n //\n // Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif\n //\n // Solution:\n //\n // We assume the radius of the polygon is half the size of the canvas at first\n // at each index we check if the text overlaps.\n //\n // Where it does, we store that angle and that index.\n //\n // After finding the largest index and angle we calculate how much we need to remove\n // from the shape radius to move the point inwards by that x.\n //\n // We average the left and right distances to get the maximum shape radius that can fit in the box\n // along with labels.\n //\n // Once we have that, we can find the centre point for the chart, by taking the x text protrusion\n // on each side, removing that from the size, halving it and adding the left x protrusion width.\n //\n // This will mean we have a shape fitted to the canvas, as large as it can be with the labels\n // and position it in the most space efficient manner\n //\n // https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif\n\n // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.\n // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points\n const orig = {\n l: scale.left + scale._padding.left,\n r: scale.right - scale._padding.right,\n t: scale.top + scale._padding.top,\n b: scale.bottom - scale._padding.bottom\n };\n const limits = Object.assign({}, orig);\n const labelSizes = [];\n const padding = [];\n const valueCount = scale._pointLabels.length;\n const pointLabelOpts = scale.options.pointLabels;\n const additionalAngle = pointLabelOpts.centerPointLabels ? PI / valueCount : 0;\n\n for (let i = 0; i < valueCount; i++) {\n const opts = pointLabelOpts.setContext(scale.getPointLabelContext(i));\n padding[i] = opts.padding;\n const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i], additionalAngle);\n const plFont = toFont(opts.font);\n const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]);\n labelSizes[i] = textSize;\n\n const angleRadians = _normalizeAngle(scale.getIndexAngle(i) + additionalAngle);\n const angle = Math.round(toDegrees(angleRadians));\n const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180);\n const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270);\n updateLimits(limits, orig, angleRadians, hLimits, vLimits);\n }\n\n scale.setCenterPoint(\n orig.l - limits.l,\n limits.r - orig.r,\n orig.t - limits.t,\n limits.b - orig.b\n );\n\n // Now that text size is determined, compute the full positions\n scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding);\n}\n\nfunction updateLimits(limits, orig, angle, hLimits, vLimits) {\n const sin = Math.abs(Math.sin(angle));\n const cos = Math.abs(Math.cos(angle));\n let x = 0;\n let y = 0;\n if (hLimits.start < orig.l) {\n x = (orig.l - hLimits.start) / sin;\n limits.l = Math.min(limits.l, orig.l - x);\n } else if (hLimits.end > orig.r) {\n x = (hLimits.end - orig.r) / sin;\n limits.r = Math.max(limits.r, orig.r + x);\n }\n if (vLimits.start < orig.t) {\n y = (orig.t - vLimits.start) / cos;\n limits.t = Math.min(limits.t, orig.t - y);\n } else if (vLimits.end > orig.b) {\n y = (vLimits.end - orig.b) / cos;\n limits.b = Math.max(limits.b, orig.b + y);\n }\n}\n\nfunction createPointLabelItem(scale, index, itemOpts) {\n const outerDistance = scale.drawingArea;\n const {extra, additionalAngle, padding, size} = itemOpts;\n const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle);\n const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI)));\n const y = yForAngle(pointLabelPosition.y, size.h, angle);\n const textAlign = getTextAlignForAngle(angle);\n const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign);\n return {\n // if to draw or overlapped\n visible: true,\n\n // Text position\n x: pointLabelPosition.x,\n y,\n\n // Text rendering data\n textAlign,\n\n // Bounding box\n left,\n top: y,\n right: left + size.w,\n bottom: y + size.h\n };\n}\n\nfunction isNotOverlapped(item, area) {\n if (!area) {\n return true;\n }\n const {left, top, right, bottom} = item;\n const apexesInArea = _isPointInArea({x: left, y: top}, area) || _isPointInArea({x: left, y: bottom}, area) ||\n _isPointInArea({x: right, y: top}, area) || _isPointInArea({x: right, y: bottom}, area);\n return !apexesInArea;\n}\n\nfunction buildPointLabelItems(scale, labelSizes, padding) {\n const items = [];\n const valueCount = scale._pointLabels.length;\n const opts = scale.options;\n const {centerPointLabels, display} = opts.pointLabels;\n const itemOpts = {\n extra: getTickBackdropHeight(opts) / 2,\n additionalAngle: centerPointLabels ? PI / valueCount : 0\n };\n let area;\n\n for (let i = 0; i < valueCount; i++) {\n itemOpts.padding = padding[i];\n itemOpts.size = labelSizes[i];\n\n const item = createPointLabelItem(scale, i, itemOpts);\n items.push(item);\n if (display === 'auto') {\n item.visible = isNotOverlapped(item, area);\n if (item.visible) {\n area = item;\n }\n }\n }\n return items;\n}\n\nfunction getTextAlignForAngle(angle) {\n if (angle === 0 || angle === 180) {\n return 'center';\n } else if (angle < 180) {\n return 'left';\n }\n\n return 'right';\n}\n\nfunction leftForTextAlign(x, w, align) {\n if (align === 'right') {\n x -= w;\n } else if (align === 'center') {\n x -= (w / 2);\n }\n return x;\n}\n\nfunction yForAngle(y, h, angle) {\n if (angle === 90 || angle === 270) {\n y -= (h / 2);\n } else if (angle > 270 || angle < 90) {\n y -= h;\n }\n return y;\n}\n\nfunction drawPointLabelBox(ctx, opts, item) {\n const {left, top, right, bottom} = item;\n const {backdropColor} = opts;\n\n if (!isNullOrUndef(backdropColor)) {\n const borderRadius = toTRBLCorners(opts.borderRadius);\n const padding = toPadding(opts.backdropPadding);\n ctx.fillStyle = backdropColor;\n\n const backdropLeft = left - padding.left;\n const backdropTop = top - padding.top;\n const backdropWidth = right - left + padding.width;\n const backdropHeight = bottom - top + padding.height;\n\n if (Object.values(borderRadius).some(v => v !== 0)) {\n ctx.beginPath();\n addRoundedRectPath(ctx, {\n x: backdropLeft,\n y: backdropTop,\n w: backdropWidth,\n h: backdropHeight,\n radius: borderRadius,\n });\n ctx.fill();\n } else {\n ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight);\n }\n }\n}\n\nfunction drawPointLabels(scale, labelCount) {\n const {ctx, options: {pointLabels}} = scale;\n\n for (let i = labelCount - 1; i >= 0; i--) {\n const item = scale._pointLabelItems[i];\n if (!item.visible) {\n // overlapping\n continue;\n }\n const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i));\n drawPointLabelBox(ctx, optsAtIndex, item);\n const plFont = toFont(optsAtIndex.font);\n const {x, y, textAlign} = item;\n\n renderText(\n ctx,\n scale._pointLabels[i],\n x,\n y + (plFont.lineHeight / 2),\n plFont,\n {\n color: optsAtIndex.color,\n textAlign: textAlign,\n textBaseline: 'middle'\n }\n );\n }\n}\n\nfunction pathRadiusLine(scale, radius, circular, labelCount) {\n const {ctx} = scale;\n if (circular) {\n // Draw circular arcs between the points\n ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU);\n } else {\n // Draw straight lines connecting each index\n let pointPosition = scale.getPointPosition(0, radius);\n ctx.moveTo(pointPosition.x, pointPosition.y);\n\n for (let i = 1; i < labelCount; i++) {\n pointPosition = scale.getPointPosition(i, radius);\n ctx.lineTo(pointPosition.x, pointPosition.y);\n }\n }\n}\n\nfunction drawRadiusLine(scale, gridLineOpts, radius, labelCount, borderOpts) {\n const ctx = scale.ctx;\n const circular = gridLineOpts.circular;\n\n const {color, lineWidth} = gridLineOpts;\n\n if ((!circular && !labelCount) || !color || !lineWidth || radius < 0) {\n return;\n }\n\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.setLineDash(borderOpts.dash || []);\n ctx.lineDashOffset = borderOpts.dashOffset;\n\n ctx.beginPath();\n pathRadiusLine(scale, radius, circular, labelCount);\n ctx.closePath();\n ctx.stroke();\n ctx.restore();\n}\n\nfunction createPointLabelContext(parent, index, label) {\n return createContext(parent, {\n label,\n index,\n type: 'pointLabel'\n });\n}\n\nexport default class RadialLinearScale extends LinearScaleBase {\n\n static id = 'radialLinear';\n\n /**\n * @type {any}\n */\n static defaults = {\n display: true,\n\n // Boolean - Whether to animate scaling the chart from the centre\n animate: true,\n position: 'chartArea',\n\n angleLines: {\n display: true,\n lineWidth: 1,\n borderDash: [],\n borderDashOffset: 0.0\n },\n\n grid: {\n circular: false\n },\n\n startAngle: 0,\n\n // label settings\n ticks: {\n // Boolean - Show a backdrop to the scale label\n showLabelBackdrop: true,\n\n callback: Ticks.formatters.numeric\n },\n\n pointLabels: {\n backdropColor: undefined,\n\n // Number - The backdrop padding above & below the label in pixels\n backdropPadding: 2,\n\n // Boolean - if true, show point labels\n display: true,\n\n // Number - Point label font size in pixels\n font: {\n size: 10\n },\n\n // Function - Used to convert point labels\n callback(label) {\n return label;\n },\n\n // Number - Additionl padding between scale and pointLabel\n padding: 5,\n\n // Boolean - if true, center point labels to slices in polar chart\n centerPointLabels: false\n }\n };\n\n static defaultRoutes = {\n 'angleLines.color': 'borderColor',\n 'pointLabels.color': 'color',\n 'ticks.color': 'color'\n };\n\n static descriptors = {\n angleLines: {\n _fallback: 'grid'\n }\n };\n\n constructor(cfg) {\n super(cfg);\n\n /** @type {number} */\n this.xCenter = undefined;\n /** @type {number} */\n this.yCenter = undefined;\n /** @type {number} */\n this.drawingArea = undefined;\n /** @type {string[]} */\n this._pointLabels = [];\n this._pointLabelItems = [];\n }\n\n setDimensions() {\n // Set the unconstrained dimension before label rotation\n const padding = this._padding = toPadding(getTickBackdropHeight(this.options) / 2);\n const w = this.width = this.maxWidth - padding.width;\n const h = this.height = this.maxHeight - padding.height;\n this.xCenter = Math.floor(this.left + w / 2 + padding.left);\n this.yCenter = Math.floor(this.top + h / 2 + padding.top);\n this.drawingArea = Math.floor(Math.min(w, h) / 2);\n }\n\n determineDataLimits() {\n const {min, max} = this.getMinMax(false);\n\n this.min = isFinite(min) && !isNaN(min) ? min : 0;\n this.max = isFinite(max) && !isNaN(max) ? max : 0;\n\n // Common base implementation to handle min, max, beginAtZero\n this.handleTickRangeOptions();\n }\n\n /**\n\t * Returns the maximum number of ticks based on the scale dimension\n\t * @protected\n\t */\n computeTickLimit() {\n return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options));\n }\n\n generateTickLabels(ticks) {\n LinearScaleBase.prototype.generateTickLabels.call(this, ticks);\n\n // Point labels\n this._pointLabels = this.getLabels()\n .map((value, index) => {\n const label = callCallback(this.options.pointLabels.callback, [value, index], this);\n return label || label === 0 ? label : '';\n })\n .filter((v, i) => this.chart.getDataVisibility(i));\n }\n\n fit() {\n const opts = this.options;\n\n if (opts.display && opts.pointLabels.display) {\n fitWithPointLabels(this);\n } else {\n this.setCenterPoint(0, 0, 0, 0);\n }\n }\n\n setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) {\n this.xCenter += Math.floor((leftMovement - rightMovement) / 2);\n this.yCenter += Math.floor((topMovement - bottomMovement) / 2);\n this.drawingArea -= Math.min(this.drawingArea / 2, Math.max(leftMovement, rightMovement, topMovement, bottomMovement));\n }\n\n getIndexAngle(index) {\n const angleMultiplier = TAU / (this._pointLabels.length || 1);\n const startAngle = this.options.startAngle || 0;\n\n return _normalizeAngle(index * angleMultiplier + toRadians(startAngle));\n }\n\n getDistanceFromCenterForValue(value) {\n if (isNullOrUndef(value)) {\n return NaN;\n }\n\n // Take into account half font size + the yPadding of the top value\n const scalingFactor = this.drawingArea / (this.max - this.min);\n if (this.options.reverse) {\n return (this.max - value) * scalingFactor;\n }\n return (value - this.min) * scalingFactor;\n }\n\n getValueForDistanceFromCenter(distance) {\n if (isNullOrUndef(distance)) {\n return NaN;\n }\n\n const scaledDistance = distance / (this.drawingArea / (this.max - this.min));\n return this.options.reverse ? this.max - scaledDistance : this.min + scaledDistance;\n }\n\n getPointLabelContext(index) {\n const pointLabels = this._pointLabels || [];\n\n if (index >= 0 && index < pointLabels.length) {\n const pointLabel = pointLabels[index];\n return createPointLabelContext(this.getContext(), index, pointLabel);\n }\n }\n\n getPointPosition(index, distanceFromCenter, additionalAngle = 0) {\n const angle = this.getIndexAngle(index) - HALF_PI + additionalAngle;\n return {\n x: Math.cos(angle) * distanceFromCenter + this.xCenter,\n y: Math.sin(angle) * distanceFromCenter + this.yCenter,\n angle\n };\n }\n\n getPointPositionForValue(index, value) {\n return this.getPointPosition(index, this.getDistanceFromCenterForValue(value));\n }\n\n getBasePosition(index) {\n return this.getPointPositionForValue(index || 0, this.getBaseValue());\n }\n\n getPointLabelPosition(index) {\n const {left, top, right, bottom} = this._pointLabelItems[index];\n return {\n left,\n top,\n right,\n bottom,\n };\n }\n\n /**\n\t * @protected\n\t */\n drawBackground() {\n const {backgroundColor, grid: {circular}} = this.options;\n if (backgroundColor) {\n const ctx = this.ctx;\n ctx.save();\n ctx.beginPath();\n pathRadiusLine(this, this.getDistanceFromCenterForValue(this._endValue), circular, this._pointLabels.length);\n ctx.closePath();\n ctx.fillStyle = backgroundColor;\n ctx.fill();\n ctx.restore();\n }\n }\n\n /**\n\t * @protected\n\t */\n drawGrid() {\n const ctx = this.ctx;\n const opts = this.options;\n const {angleLines, grid, border} = opts;\n const labelCount = this._pointLabels.length;\n\n let i, offset, position;\n\n if (opts.pointLabels.display) {\n drawPointLabels(this, labelCount);\n }\n\n if (grid.display) {\n this.ticks.forEach((tick, index) => {\n if (index !== 0 || (index === 0 && this.min < 0)) {\n offset = this.getDistanceFromCenterForValue(tick.value);\n const context = this.getContext(index);\n const optsAtIndex = grid.setContext(context);\n const optsAtIndexBorder = border.setContext(context);\n\n drawRadiusLine(this, optsAtIndex, offset, labelCount, optsAtIndexBorder);\n }\n });\n }\n\n if (angleLines.display) {\n ctx.save();\n\n for (i = labelCount - 1; i >= 0; i--) {\n const optsAtIndex = angleLines.setContext(this.getPointLabelContext(i));\n const {color, lineWidth} = optsAtIndex;\n\n if (!lineWidth || !color) {\n continue;\n }\n\n ctx.lineWidth = lineWidth;\n ctx.strokeStyle = color;\n\n ctx.setLineDash(optsAtIndex.borderDash);\n ctx.lineDashOffset = optsAtIndex.borderDashOffset;\n\n offset = this.getDistanceFromCenterForValue(opts.reverse ? this.min : this.max);\n position = this.getPointPosition(i, offset);\n ctx.beginPath();\n ctx.moveTo(this.xCenter, this.yCenter);\n ctx.lineTo(position.x, position.y);\n ctx.stroke();\n }\n\n ctx.restore();\n }\n }\n\n /**\n\t * @protected\n\t */\n drawBorder() {}\n\n /**\n\t * @protected\n\t */\n drawLabels() {\n const ctx = this.ctx;\n const opts = this.options;\n const tickOpts = opts.ticks;\n\n if (!tickOpts.display) {\n return;\n }\n\n const startAngle = this.getIndexAngle(0);\n let offset, width;\n\n ctx.save();\n ctx.translate(this.xCenter, this.yCenter);\n ctx.rotate(startAngle);\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n\n this.ticks.forEach((tick, index) => {\n if ((index === 0 && this.min >= 0) && !opts.reverse) {\n return;\n }\n\n const optsAtIndex = tickOpts.setContext(this.getContext(index));\n const tickFont = toFont(optsAtIndex.font);\n offset = this.getDistanceFromCenterForValue(this.ticks[index].value);\n\n if (optsAtIndex.showLabelBackdrop) {\n ctx.font = tickFont.string;\n width = ctx.measureText(tick.label).width;\n ctx.fillStyle = optsAtIndex.backdropColor;\n\n const padding = toPadding(optsAtIndex.backdropPadding);\n ctx.fillRect(\n -width / 2 - padding.left,\n -offset - tickFont.size / 2 - padding.top,\n width + padding.width,\n tickFont.size + padding.height\n );\n }\n\n renderText(ctx, tick.label, 0, -offset, tickFont, {\n color: optsAtIndex.color,\n strokeColor: optsAtIndex.textStrokeColor,\n strokeWidth: optsAtIndex.textStrokeWidth,\n });\n });\n\n ctx.restore();\n }\n\n /**\n\t * @protected\n\t */\n drawTitle() {}\n}\n","import adapters from '../core/core.adapters.js';\nimport {callback as call, isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpers.core.js';\nimport {toRadians, isNumber, _limitValue} from '../helpers/helpers.math.js';\nimport Scale from '../core/core.scale.js';\nimport {_arrayUnique, _filterBetween, _lookup} from '../helpers/helpers.collection.js';\n\n/**\n * @typedef { import('../core/core.adapters.js').TimeUnit } Unit\n * @typedef {{common: boolean, size: number, steps?: number}} Interval\n * @typedef { import('../core/core.adapters.js').DateAdapter } DateAdapter\n */\n\n/**\n * @type {Object}\n */\nconst INTERVALS = {\n millisecond: {common: true, size: 1, steps: 1000},\n second: {common: true, size: 1000, steps: 60},\n minute: {common: true, size: 60000, steps: 60},\n hour: {common: true, size: 3600000, steps: 24},\n day: {common: true, size: 86400000, steps: 30},\n week: {common: false, size: 604800000, steps: 4},\n month: {common: true, size: 2.628e9, steps: 12},\n quarter: {common: false, size: 7.884e9, steps: 4},\n year: {common: true, size: 3.154e10}\n};\n\n/**\n * @type {Unit[]}\n */\nconst UNITS = /** @type Unit[] */ /* #__PURE__ */ (Object.keys(INTERVALS));\n\n/**\n * @param {number} a\n * @param {number} b\n */\nfunction sorter(a, b) {\n return a - b;\n}\n\n/**\n * @param {TimeScale} scale\n * @param {*} input\n * @return {number}\n */\nfunction parse(scale, input) {\n if (isNullOrUndef(input)) {\n return null;\n }\n\n const adapter = scale._adapter;\n const {parser, round, isoWeekday} = scale._parseOpts;\n let value = input;\n\n if (typeof parser === 'function') {\n value = parser(value);\n }\n\n // Only parse if it's not a timestamp already\n if (!isFinite(value)) {\n value = typeof parser === 'string'\n ? adapter.parse(value, /** @type {Unit} */ (parser))\n : adapter.parse(value);\n }\n\n if (value === null) {\n return null;\n }\n\n if (round) {\n value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true)\n ? adapter.startOf(value, 'isoWeek', isoWeekday)\n : adapter.startOf(value, round);\n }\n\n return +value;\n}\n\n/**\n * Figures out what unit results in an appropriate number of auto-generated ticks\n * @param {Unit} minUnit\n * @param {number} min\n * @param {number} max\n * @param {number} capacity\n * @return {object}\n */\nfunction determineUnitForAutoTicks(minUnit, min, max, capacity) {\n const ilen = UNITS.length;\n\n for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {\n const interval = INTERVALS[UNITS[i]];\n const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER;\n\n if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) {\n return UNITS[i];\n }\n }\n\n return UNITS[ilen - 1];\n}\n\n/**\n * Figures out what unit to format a set of ticks with\n * @param {TimeScale} scale\n * @param {number} numTicks\n * @param {Unit} minUnit\n * @param {number} min\n * @param {number} max\n * @return {Unit}\n */\nfunction determineUnitForFormatting(scale, numTicks, minUnit, min, max) {\n for (let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) {\n const unit = UNITS[i];\n if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) {\n return unit;\n }\n }\n\n return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0];\n}\n\n/**\n * @param {Unit} unit\n * @return {object}\n */\nfunction determineMajorUnit(unit) {\n for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) {\n if (INTERVALS[UNITS[i]].common) {\n return UNITS[i];\n }\n }\n}\n\n/**\n * @param {object} ticks\n * @param {number} time\n * @param {number[]} [timestamps] - if defined, snap to these timestamps\n */\nfunction addTick(ticks, time, timestamps) {\n if (!timestamps) {\n ticks[time] = true;\n } else if (timestamps.length) {\n const {lo, hi} = _lookup(timestamps, time);\n const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi];\n ticks[timestamp] = true;\n }\n}\n\n/**\n * @param {TimeScale} scale\n * @param {object[]} ticks\n * @param {object} map\n * @param {Unit} majorUnit\n * @return {object[]}\n */\nfunction setMajorTicks(scale, ticks, map, majorUnit) {\n const adapter = scale._adapter;\n const first = +adapter.startOf(ticks[0].value, majorUnit);\n const last = ticks[ticks.length - 1].value;\n let major, index;\n\n for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) {\n index = map[major];\n if (index >= 0) {\n ticks[index].major = true;\n }\n }\n return ticks;\n}\n\n/**\n * @param {TimeScale} scale\n * @param {number[]} values\n * @param {Unit|undefined} [majorUnit]\n * @return {object[]}\n */\nfunction ticksFromTimestamps(scale, values, majorUnit) {\n const ticks = [];\n /** @type {Object} */\n const map = {};\n const ilen = values.length;\n let i, value;\n\n for (i = 0; i < ilen; ++i) {\n value = values[i];\n map[value] = i;\n\n ticks.push({\n value,\n major: false\n });\n }\n\n // We set the major ticks separately from the above loop because calling startOf for every tick\n // is expensive when there is a large number of ticks\n return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit);\n}\n\nexport default class TimeScale extends Scale {\n\n static id = 'time';\n\n /**\n * @type {any}\n */\n static defaults = {\n /**\n * Scale boundary strategy (bypassed by min/max time options)\n * - `data`: make sure data are fully visible, ticks outside are removed\n * - `ticks`: make sure ticks are fully visible, data outside are truncated\n * @see https://github.com/chartjs/Chart.js/pull/4556\n * @since 2.7.0\n */\n bounds: 'data',\n\n adapters: {},\n time: {\n parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp\n unit: false, // false == automatic or override with week, month, year, etc.\n round: false, // none, or override with week, month, year, etc.\n isoWeekday: false, // override week start day\n minUnit: 'millisecond',\n displayFormats: {}\n },\n ticks: {\n /**\n * Ticks generation input values:\n * - 'auto': generates \"optimal\" ticks based on scale size and time options.\n * - 'data': generates ticks from data (including labels from data {t|x|y} objects).\n * - 'labels': generates ticks from user given `data.labels` values ONLY.\n * @see https://github.com/chartjs/Chart.js/pull/4507\n * @since 2.7.0\n */\n source: 'auto',\n\n callback: false,\n\n major: {\n enabled: false\n }\n }\n };\n\n /**\n\t * @param {object} props\n\t */\n constructor(props) {\n super(props);\n\n /** @type {{data: number[], labels: number[], all: number[]}} */\n this._cache = {\n data: [],\n labels: [],\n all: []\n };\n\n /** @type {Unit} */\n this._unit = 'day';\n /** @type {Unit=} */\n this._majorUnit = undefined;\n this._offsets = {};\n this._normalized = false;\n this._parseOpts = undefined;\n }\n\n init(scaleOpts, opts = {}) {\n const time = scaleOpts.time || (scaleOpts.time = {});\n /** @type {DateAdapter} */\n const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date);\n\n adapter.init(opts);\n\n // Backward compatibility: before introducing adapter, `displayFormats` was\n // supposed to contain *all* unit/string pairs but this can't be resolved\n // when loading the scale (adapters are loaded afterward), so let's populate\n // missing formats on update\n mergeIf(time.displayFormats, adapter.formats());\n\n this._parseOpts = {\n parser: time.parser,\n round: time.round,\n isoWeekday: time.isoWeekday\n };\n\n super.init(scaleOpts);\n\n this._normalized = opts.normalized;\n }\n\n /**\n\t * @param {*} raw\n\t * @param {number?} [index]\n\t * @return {number}\n\t */\n parse(raw, index) { // eslint-disable-line no-unused-vars\n if (raw === undefined) {\n return null;\n }\n return parse(this, raw);\n }\n\n beforeLayout() {\n super.beforeLayout();\n this._cache = {\n data: [],\n labels: [],\n all: []\n };\n }\n\n determineDataLimits() {\n const options = this.options;\n const adapter = this._adapter;\n const unit = options.time.unit || 'day';\n // eslint-disable-next-line prefer-const\n let {min, max, minDefined, maxDefined} = this.getUserBounds();\n\n /**\n\t\t * @param {object} bounds\n\t\t */\n function _applyBounds(bounds) {\n if (!minDefined && !isNaN(bounds.min)) {\n min = Math.min(min, bounds.min);\n }\n if (!maxDefined && !isNaN(bounds.max)) {\n max = Math.max(max, bounds.max);\n }\n }\n\n // If we have user provided `min` and `max` labels / data bounds can be ignored\n if (!minDefined || !maxDefined) {\n // Labels are always considered, when user did not force bounds\n _applyBounds(this._getLabelBounds());\n\n // If `bounds` is `'ticks'` and `ticks.source` is `'labels'`,\n // data bounds are ignored (and don't need to be determined)\n if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') {\n _applyBounds(this.getMinMax(false));\n }\n }\n\n min = isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit);\n max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1;\n\n // Make sure that max is strictly higher than min (required by the timeseries lookup table)\n this.min = Math.min(min, max - 1);\n this.max = Math.max(min + 1, max);\n }\n\n /**\n\t * @private\n\t */\n _getLabelBounds() {\n const arr = this.getLabelTimestamps();\n let min = Number.POSITIVE_INFINITY;\n let max = Number.NEGATIVE_INFINITY;\n\n if (arr.length) {\n min = arr[0];\n max = arr[arr.length - 1];\n }\n return {min, max};\n }\n\n /**\n\t * @return {object[]}\n\t */\n buildTicks() {\n const options = this.options;\n const timeOpts = options.time;\n const tickOpts = options.ticks;\n const timestamps = tickOpts.source === 'labels' ? this.getLabelTimestamps() : this._generate();\n\n if (options.bounds === 'ticks' && timestamps.length) {\n this.min = this._userMin || timestamps[0];\n this.max = this._userMax || timestamps[timestamps.length - 1];\n }\n\n const min = this.min;\n const max = this.max;\n\n const ticks = _filterBetween(timestamps, min, max);\n\n // PRIVATE\n // determineUnitForFormatting relies on the number of ticks so we don't use it when\n // autoSkip is enabled because we don't yet know what the final number of ticks will be\n this._unit = timeOpts.unit || (tickOpts.autoSkip\n ? determineUnitForAutoTicks(timeOpts.minUnit, this.min, this.max, this._getLabelCapacity(min))\n : determineUnitForFormatting(this, ticks.length, timeOpts.minUnit, this.min, this.max));\n this._majorUnit = !tickOpts.major.enabled || this._unit === 'year' ? undefined\n : determineMajorUnit(this._unit);\n this.initOffsets(timestamps);\n\n if (options.reverse) {\n ticks.reverse();\n }\n\n return ticksFromTimestamps(this, ticks, this._majorUnit);\n }\n\n afterAutoSkip() {\n // Offsets for bar charts need to be handled with the auto skipped\n // ticks. Once ticks have been skipped, we re-compute the offsets.\n if (this.options.offsetAfterAutoskip) {\n this.initOffsets(this.ticks.map(tick => +tick.value));\n }\n }\n\n /**\n\t * Returns the start and end offsets from edges in the form of {start, end}\n\t * where each value is a relative width to the scale and ranges between 0 and 1.\n\t * They add extra margins on the both sides by scaling down the original scale.\n\t * Offsets are added when the `offset` option is true.\n\t * @param {number[]} timestamps\n\t * @protected\n\t */\n initOffsets(timestamps = []) {\n let start = 0;\n let end = 0;\n let first, last;\n\n if (this.options.offset && timestamps.length) {\n first = this.getDecimalForValue(timestamps[0]);\n if (timestamps.length === 1) {\n start = 1 - first;\n } else {\n start = (this.getDecimalForValue(timestamps[1]) - first) / 2;\n }\n last = this.getDecimalForValue(timestamps[timestamps.length - 1]);\n if (timestamps.length === 1) {\n end = last;\n } else {\n end = (last - this.getDecimalForValue(timestamps[timestamps.length - 2])) / 2;\n }\n }\n const limit = timestamps.length < 3 ? 0.5 : 0.25;\n start = _limitValue(start, 0, limit);\n end = _limitValue(end, 0, limit);\n\n this._offsets = {start, end, factor: 1 / (start + 1 + end)};\n }\n\n /**\n\t * Generates a maximum of `capacity` timestamps between min and max, rounded to the\n\t * `minor` unit using the given scale time `options`.\n\t * Important: this method can return ticks outside the min and max range, it's the\n\t * responsibility of the calling code to clamp values if needed.\n\t * @protected\n\t */\n _generate() {\n const adapter = this._adapter;\n const min = this.min;\n const max = this.max;\n const options = this.options;\n const timeOpts = options.time;\n // @ts-ignore\n const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, this._getLabelCapacity(min));\n const stepSize = valueOrDefault(options.ticks.stepSize, 1);\n const weekday = minor === 'week' ? timeOpts.isoWeekday : false;\n const hasWeekday = isNumber(weekday) || weekday === true;\n const ticks = {};\n let first = min;\n let time, count;\n\n // For 'week' unit, handle the first day of week option\n if (hasWeekday) {\n first = +adapter.startOf(first, 'isoWeek', weekday);\n }\n\n // Align first ticks on unit\n first = +adapter.startOf(first, hasWeekday ? 'day' : minor);\n\n // Prevent browser from freezing in case user options request millions of milliseconds\n if (adapter.diff(max, min, minor) > 100000 * stepSize) {\n throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor);\n }\n\n const timestamps = options.ticks.source === 'data' && this.getDataTimestamps();\n for (time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++) {\n addTick(ticks, time, timestamps);\n }\n\n if (time === max || options.bounds === 'ticks' || count === 1) {\n addTick(ticks, time, timestamps);\n }\n\n // @ts-ignore\n return Object.keys(ticks).sort(sorter).map(x => +x);\n }\n\n /**\n\t * @param {number} value\n\t * @return {string}\n\t */\n getLabelForValue(value) {\n const adapter = this._adapter;\n const timeOpts = this.options.time;\n\n if (timeOpts.tooltipFormat) {\n return adapter.format(value, timeOpts.tooltipFormat);\n }\n return adapter.format(value, timeOpts.displayFormats.datetime);\n }\n\n /**\n\t * @param {number} value\n\t * @param {string|undefined} format\n\t * @return {string}\n\t */\n format(value, format) {\n const options = this.options;\n const formats = options.time.displayFormats;\n const unit = this._unit;\n const fmt = format || formats[unit];\n return this._adapter.format(value, fmt);\n }\n\n /**\n\t * Function to format an individual tick mark\n\t * @param {number} time\n\t * @param {number} index\n\t * @param {object[]} ticks\n\t * @param {string|undefined} [format]\n\t * @return {string}\n\t * @private\n\t */\n _tickFormatFunction(time, index, ticks, format) {\n const options = this.options;\n const formatter = options.ticks.callback;\n\n if (formatter) {\n return call(formatter, [time, index, ticks], this);\n }\n\n const formats = options.time.displayFormats;\n const unit = this._unit;\n const majorUnit = this._majorUnit;\n const minorFormat = unit && formats[unit];\n const majorFormat = majorUnit && formats[majorUnit];\n const tick = ticks[index];\n const major = majorUnit && majorFormat && tick && tick.major;\n\n return this._adapter.format(time, format || (major ? majorFormat : minorFormat));\n }\n\n /**\n\t * @param {object[]} ticks\n\t */\n generateTickLabels(ticks) {\n let i, ilen, tick;\n\n for (i = 0, ilen = ticks.length; i < ilen; ++i) {\n tick = ticks[i];\n tick.label = this._tickFormatFunction(tick.value, i, ticks);\n }\n }\n\n /**\n\t * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)\n\t * @return {number}\n\t */\n getDecimalForValue(value) {\n return value === null ? NaN : (value - this.min) / (this.max - this.min);\n }\n\n /**\n\t * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)\n\t * @return {number}\n\t */\n getPixelForValue(value) {\n const offsets = this._offsets;\n const pos = this.getDecimalForValue(value);\n return this.getPixelForDecimal((offsets.start + pos) * offsets.factor);\n }\n\n /**\n\t * @param {number} pixel\n\t * @return {number}\n\t */\n getValueForPixel(pixel) {\n const offsets = this._offsets;\n const pos = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end;\n return this.min + pos * (this.max - this.min);\n }\n\n /**\n\t * @param {string} label\n\t * @return {{w:number, h:number}}\n\t * @private\n\t */\n _getLabelSize(label) {\n const ticksOpts = this.options.ticks;\n const tickLabelWidth = this.ctx.measureText(label).width;\n const angle = toRadians(this.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation);\n const cosRotation = Math.cos(angle);\n const sinRotation = Math.sin(angle);\n const tickFontSize = this._resolveTickFontOptions(0).size;\n\n return {\n w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation),\n h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation)\n };\n }\n\n /**\n\t * @param {number} exampleTime\n\t * @return {number}\n\t * @private\n\t */\n _getLabelCapacity(exampleTime) {\n const timeOpts = this.options.time;\n const displayFormats = timeOpts.displayFormats;\n\n // pick the longest format (milliseconds) for guesstimation\n const format = displayFormats[timeOpts.unit] || displayFormats.millisecond;\n const exampleLabel = this._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(this, [exampleTime], this._majorUnit), format);\n const size = this._getLabelSize(exampleLabel);\n // subtract 1 - if offset then there's one less label than tick\n // if not offset then one half label padding is added to each end leaving room for one less label\n const capacity = Math.floor(this.isHorizontal() ? this.width / size.w : this.height / size.h) - 1;\n return capacity > 0 ? capacity : 1;\n }\n\n /**\n\t * @protected\n\t */\n getDataTimestamps() {\n let timestamps = this._cache.data || [];\n let i, ilen;\n\n if (timestamps.length) {\n return timestamps;\n }\n\n const metas = this.getMatchingVisibleMetas();\n\n if (this._normalized && metas.length) {\n return (this._cache.data = metas[0].controller.getAllParsedValues(this));\n }\n\n for (i = 0, ilen = metas.length; i < ilen; ++i) {\n timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(this));\n }\n\n return (this._cache.data = this.normalize(timestamps));\n }\n\n /**\n\t * @protected\n\t */\n getLabelTimestamps() {\n const timestamps = this._cache.labels || [];\n let i, ilen;\n\n if (timestamps.length) {\n return timestamps;\n }\n\n const labels = this.getLabels();\n for (i = 0, ilen = labels.length; i < ilen; ++i) {\n timestamps.push(parse(this, labels[i]));\n }\n\n return (this._cache.labels = this._normalized ? timestamps : this.normalize(timestamps));\n }\n\n /**\n\t * @param {number[]} values\n\t * @protected\n\t */\n normalize(values) {\n // It seems to be somewhat faster to do sorting first\n return _arrayUnique(values.sort(sorter));\n }\n}\n","import TimeScale from './scale.time.js';\nimport {_lookupByKey} from '../helpers/helpers.collection.js';\n\n/**\n * Linearly interpolates the given source `val` using the table. If value is out of bounds, values\n * at edges are used for the interpolation.\n * @param {object} table\n * @param {number} val\n * @param {boolean} [reverse] lookup time based on position instead of vice versa\n * @return {object}\n */\nfunction interpolate(table, val, reverse) {\n let lo = 0;\n let hi = table.length - 1;\n let prevSource, nextSource, prevTarget, nextTarget;\n if (reverse) {\n if (val >= table[lo].pos && val <= table[hi].pos) {\n ({lo, hi} = _lookupByKey(table, 'pos', val));\n }\n ({pos: prevSource, time: prevTarget} = table[lo]);\n ({pos: nextSource, time: nextTarget} = table[hi]);\n } else {\n if (val >= table[lo].time && val <= table[hi].time) {\n ({lo, hi} = _lookupByKey(table, 'time', val));\n }\n ({time: prevSource, pos: prevTarget} = table[lo]);\n ({time: nextSource, pos: nextTarget} = table[hi]);\n }\n\n const span = nextSource - prevSource;\n return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget;\n}\n\nclass TimeSeriesScale extends TimeScale {\n\n static id = 'timeseries';\n\n /**\n * @type {any}\n */\n static defaults = TimeScale.defaults;\n\n /**\n\t * @param {object} props\n\t */\n constructor(props) {\n super(props);\n\n /** @type {object[]} */\n this._table = [];\n /** @type {number} */\n this._minPos = undefined;\n /** @type {number} */\n this._tableRange = undefined;\n }\n\n /**\n\t * @protected\n\t */\n initOffsets() {\n const timestamps = this._getTimestampsForTable();\n const table = this._table = this.buildLookupTable(timestamps);\n this._minPos = interpolate(table, this.min);\n this._tableRange = interpolate(table, this.max) - this._minPos;\n super.initOffsets(timestamps);\n }\n\n /**\n\t * Returns an array of {time, pos} objects used to interpolate a specific `time` or position\n\t * (`pos`) on the scale, by searching entries before and after the requested value. `pos` is\n\t * a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other\n\t * extremity (left + width or top + height). Note that it would be more optimized to directly\n\t * store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need\n\t * to create the lookup table. The table ALWAYS contains at least two items: min and max.\n\t * @param {number[]} timestamps\n\t * @return {object[]}\n\t * @protected\n\t */\n buildLookupTable(timestamps) {\n const {min, max} = this;\n const items = [];\n const table = [];\n let i, ilen, prev, curr, next;\n\n for (i = 0, ilen = timestamps.length; i < ilen; ++i) {\n curr = timestamps[i];\n if (curr >= min && curr <= max) {\n items.push(curr);\n }\n }\n\n if (items.length < 2) {\n // In case there is less that 2 timestamps between min and max, the scale is defined by min and max\n return [\n {time: min, pos: 0},\n {time: max, pos: 1}\n ];\n }\n\n for (i = 0, ilen = items.length; i < ilen; ++i) {\n next = items[i + 1];\n prev = items[i - 1];\n curr = items[i];\n\n // only add points that breaks the scale linearity\n if (Math.round((next + prev) / 2) !== curr) {\n table.push({time: curr, pos: i / (ilen - 1)});\n }\n }\n return table;\n }\n\n /**\n * Generates all timestamps defined in the data.\n * Important: this method can return ticks outside the min and max range, it's the\n * responsibility of the calling code to clamp values if needed.\n * @protected\n */\n _generate() {\n const min = this.min;\n const max = this.max;\n let timestamps = super.getDataTimestamps();\n if (!timestamps.includes(min) || !timestamps.length) {\n timestamps.splice(0, 0, min);\n }\n if (!timestamps.includes(max) || timestamps.length === 1) {\n timestamps.push(max);\n }\n return timestamps.sort((a, b) => a - b);\n }\n\n /**\n\t * Returns all timestamps\n\t * @return {number[]}\n\t * @private\n\t */\n _getTimestampsForTable() {\n let timestamps = this._cache.all || [];\n\n if (timestamps.length) {\n return timestamps;\n }\n\n const data = this.getDataTimestamps();\n const label = this.getLabelTimestamps();\n if (data.length && label.length) {\n // If combining labels and data (data might not contain all labels),\n // we need to recheck uniqueness and sort\n timestamps = this.normalize(data.concat(label));\n } else {\n timestamps = data.length ? data : label;\n }\n timestamps = this._cache.all = timestamps;\n\n return timestamps;\n }\n\n /**\n\t * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC)\n\t * @return {number}\n\t */\n getDecimalForValue(value) {\n return (interpolate(this._table, value) - this._minPos) / this._tableRange;\n }\n\n /**\n\t * @param {number} pixel\n\t * @return {number}\n\t */\n getValueForPixel(pixel) {\n const offsets = this._offsets;\n const decimal = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end;\n return interpolate(this._table, decimal * this._tableRange + this._minPos, true);\n }\n}\n\nexport default TimeSeriesScale;\n","import {DoughnutController, PolarAreaController, defaults} from '../index.js';\nimport type {Chart, ChartDataset} from '../types.js';\n\nexport interface ColorsPluginOptions {\n enabled?: boolean;\n forceOverride?: boolean;\n}\n\ninterface ColorsDescriptor {\n backgroundColor?: unknown;\n borderColor?: unknown;\n}\n\nconst BORDER_COLORS = [\n 'rgb(54, 162, 235)', // blue\n 'rgb(255, 99, 132)', // red\n 'rgb(255, 159, 64)', // orange\n 'rgb(255, 205, 86)', // yellow\n 'rgb(75, 192, 192)', // green\n 'rgb(153, 102, 255)', // purple\n 'rgb(201, 203, 207)' // grey\n];\n\n// Border colors with 50% transparency\nconst BACKGROUND_COLORS = /* #__PURE__ */ BORDER_COLORS.map(color => color.replace('rgb(', 'rgba(').replace(')', ', 0.5)'));\n\nfunction getBorderColor(i: number) {\n return BORDER_COLORS[i % BORDER_COLORS.length];\n}\n\nfunction getBackgroundColor(i: number) {\n return BACKGROUND_COLORS[i % BACKGROUND_COLORS.length];\n}\n\nfunction colorizeDefaultDataset(dataset: ChartDataset, i: number) {\n dataset.borderColor = getBorderColor(i);\n dataset.backgroundColor = getBackgroundColor(i);\n\n return ++i;\n}\n\nfunction colorizeDoughnutDataset(dataset: ChartDataset, i: number) {\n dataset.backgroundColor = dataset.data.map(() => getBorderColor(i++));\n\n return i;\n}\n\nfunction colorizePolarAreaDataset(dataset: ChartDataset, i: number) {\n dataset.backgroundColor = dataset.data.map(() => getBackgroundColor(i++));\n\n return i;\n}\n\nfunction getColorizer(chart: Chart) {\n let i = 0;\n\n return (dataset: ChartDataset, datasetIndex: number) => {\n const controller = chart.getDatasetMeta(datasetIndex).controller;\n\n if (controller instanceof DoughnutController) {\n i = colorizeDoughnutDataset(dataset, i);\n } else if (controller instanceof PolarAreaController) {\n i = colorizePolarAreaDataset(dataset, i);\n } else if (controller) {\n i = colorizeDefaultDataset(dataset, i);\n }\n };\n}\n\nfunction containsColorsDefinitions(\n descriptors: ColorsDescriptor[] | Record\n) {\n let k: number | string;\n\n for (k in descriptors) {\n if (descriptors[k].borderColor || descriptors[k].backgroundColor) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction containsColorsDefinition(\n descriptor: ColorsDescriptor\n) {\n return descriptor && (descriptor.borderColor || descriptor.backgroundColor);\n}\n\nfunction containsDefaultColorsDefenitions() {\n return defaults.borderColor !== 'rgba(0,0,0,0.1)' || defaults.backgroundColor !== 'rgba(0,0,0,0.1)';\n}\n\nexport default {\n id: 'colors',\n\n defaults: {\n enabled: true,\n forceOverride: false\n } as ColorsPluginOptions,\n\n beforeLayout(chart: Chart, _args, options: ColorsPluginOptions) {\n if (!options.enabled) {\n return;\n }\n\n const {\n data: {datasets},\n options: chartOptions\n } = chart.config;\n const {elements} = chartOptions;\n\n const containsColorDefenition = (\n containsColorsDefinitions(datasets) ||\n containsColorsDefinition(chartOptions) ||\n (elements && containsColorsDefinitions(elements)) ||\n containsDefaultColorsDefenitions());\n\n if (!options.forceOverride && containsColorDefenition) {\n return;\n }\n\n const colorizer = getColorizer(chart);\n\n datasets.forEach(colorizer);\n }\n};\n","import {_limitValue, _lookupByKey, isNullOrUndef, resolve} from '../helpers/index.js';\n\nfunction lttbDecimation(data, start, count, availableWidth, options) {\n /**\n * Implementation of the Largest Triangle Three Buckets algorithm.\n *\n * This implementation is based on the original implementation by Sveinn Steinarsson\n * in https://github.com/sveinn-steinarsson/flot-downsample/blob/master/jquery.flot.downsample.js\n *\n * The original implementation is MIT licensed.\n */\n const samples = options.samples || availableWidth;\n // There are less points than the threshold, returning the whole array\n if (samples >= count) {\n return data.slice(start, start + count);\n }\n\n const decimated = [];\n\n const bucketWidth = (count - 2) / (samples - 2);\n let sampledIndex = 0;\n const endIndex = start + count - 1;\n // Starting from offset\n let a = start;\n let i, maxAreaPoint, maxArea, area, nextA;\n\n decimated[sampledIndex++] = data[a];\n\n for (i = 0; i < samples - 2; i++) {\n let avgX = 0;\n let avgY = 0;\n let j;\n\n // Adding offset\n const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start;\n const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start;\n const avgRangeLength = avgRangeEnd - avgRangeStart;\n\n for (j = avgRangeStart; j < avgRangeEnd; j++) {\n avgX += data[j].x;\n avgY += data[j].y;\n }\n\n avgX /= avgRangeLength;\n avgY /= avgRangeLength;\n\n // Adding offset\n const rangeOffs = Math.floor(i * bucketWidth) + 1 + start;\n const rangeTo = Math.min(Math.floor((i + 1) * bucketWidth) + 1, count) + start;\n const {x: pointAx, y: pointAy} = data[a];\n\n // Note that this is changed from the original algorithm which initializes these\n // values to 1. The reason for this change is that if the area is small, nextA\n // would never be set and thus a crash would occur in the next loop as `a` would become\n // `undefined`. Since the area is always positive, but could be 0 in the case of a flat trace,\n // initializing with a negative number is the correct solution.\n maxArea = area = -1;\n\n for (j = rangeOffs; j < rangeTo; j++) {\n area = 0.5 * Math.abs(\n (pointAx - avgX) * (data[j].y - pointAy) -\n (pointAx - data[j].x) * (avgY - pointAy)\n );\n\n if (area > maxArea) {\n maxArea = area;\n maxAreaPoint = data[j];\n nextA = j;\n }\n }\n\n decimated[sampledIndex++] = maxAreaPoint;\n a = nextA;\n }\n\n // Include the last point\n decimated[sampledIndex++] = data[endIndex];\n\n return decimated;\n}\n\nfunction minMaxDecimation(data, start, count, availableWidth) {\n let avgX = 0;\n let countX = 0;\n let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY;\n const decimated = [];\n const endIndex = start + count - 1;\n\n const xMin = data[start].x;\n const xMax = data[endIndex].x;\n const dx = xMax - xMin;\n\n for (i = start; i < start + count; ++i) {\n point = data[i];\n x = (point.x - xMin) / dx * availableWidth;\n y = point.y;\n const truncX = x | 0;\n\n if (truncX === prevX) {\n // Determine `minY` / `maxY` and `avgX` while we stay within same x-position\n if (y < minY) {\n minY = y;\n minIndex = i;\n } else if (y > maxY) {\n maxY = y;\n maxIndex = i;\n }\n // For first point in group, countX is `0`, so average will be `x` / 1.\n // Use point.x here because we're computing the average data `x` value\n avgX = (countX * avgX + point.x) / ++countX;\n } else {\n // Push up to 4 points, 3 for the last interval and the first point for this interval\n const lastIndex = i - 1;\n\n if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) {\n // The interval is defined by 4 points: start, min, max, end.\n // The starting point is already considered at this point, so we need to determine which\n // of the other points to add. We need to sort these points to ensure the decimated data\n // is still sorted and then ensure there are no duplicates.\n const intermediateIndex1 = Math.min(minIndex, maxIndex);\n const intermediateIndex2 = Math.max(minIndex, maxIndex);\n\n if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) {\n decimated.push({\n ...data[intermediateIndex1],\n x: avgX,\n });\n }\n if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) {\n decimated.push({\n ...data[intermediateIndex2],\n x: avgX\n });\n }\n }\n\n // lastIndex === startIndex will occur when a range has only 1 point which could\n // happen with very uneven data\n if (i > 0 && lastIndex !== startIndex) {\n // Last point in the previous interval\n decimated.push(data[lastIndex]);\n }\n\n // Start of the new interval\n decimated.push(point);\n prevX = truncX;\n countX = 0;\n minY = maxY = y;\n minIndex = maxIndex = startIndex = i;\n }\n }\n\n return decimated;\n}\n\nfunction cleanDecimatedDataset(dataset) {\n if (dataset._decimated) {\n const data = dataset._data;\n delete dataset._decimated;\n delete dataset._data;\n Object.defineProperty(dataset, 'data', {\n configurable: true,\n enumerable: true,\n writable: true,\n value: data,\n });\n }\n}\n\nfunction cleanDecimatedData(chart) {\n chart.data.datasets.forEach((dataset) => {\n cleanDecimatedDataset(dataset);\n });\n}\n\nfunction getStartAndCountOfVisiblePointsSimplified(meta, points) {\n const pointCount = points.length;\n\n let start = 0;\n let count;\n\n const {iScale} = meta;\n const {min, max, minDefined, maxDefined} = iScale.getUserBounds();\n\n if (minDefined) {\n start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1);\n }\n if (maxDefined) {\n count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start;\n } else {\n count = pointCount - start;\n }\n\n return {start, count};\n}\n\nexport default {\n id: 'decimation',\n\n defaults: {\n algorithm: 'min-max',\n enabled: false,\n },\n\n beforeElementsUpdate: (chart, args, options) => {\n if (!options.enabled) {\n // The decimation plugin may have been previously enabled. Need to remove old `dataset._data` handlers\n cleanDecimatedData(chart);\n return;\n }\n\n // Assume the entire chart is available to show a few more points than needed\n const availableWidth = chart.width;\n\n chart.data.datasets.forEach((dataset, datasetIndex) => {\n const {_data, indexAxis} = dataset;\n const meta = chart.getDatasetMeta(datasetIndex);\n const data = _data || dataset.data;\n\n if (resolve([indexAxis, chart.options.indexAxis]) === 'y') {\n // Decimation is only supported for lines that have an X indexAxis\n return;\n }\n\n if (!meta.controller.supportsDecimation) {\n // Only line datasets are supported\n return;\n }\n\n const xAxis = chart.scales[meta.xAxisID];\n if (xAxis.type !== 'linear' && xAxis.type !== 'time') {\n // Only linear interpolation is supported\n return;\n }\n\n if (chart.options.parsing) {\n // Plugin only supports data that does not need parsing\n return;\n }\n\n let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data);\n const threshold = options.threshold || 4 * availableWidth;\n if (count <= threshold) {\n // No decimation is required until we are above this threshold\n cleanDecimatedDataset(dataset);\n return;\n }\n\n if (isNullOrUndef(_data)) {\n // First time we are seeing this dataset\n // We override the 'data' property with a setter that stores the\n // raw data in _data, but reads the decimated data from _decimated\n dataset._data = data;\n delete dataset.data;\n Object.defineProperty(dataset, 'data', {\n configurable: true,\n enumerable: true,\n get: function() {\n return this._decimated;\n },\n set: function(d) {\n this._data = d;\n }\n });\n }\n\n // Point the chart to the decimated data\n let decimated;\n switch (options.algorithm) {\n case 'lttb':\n decimated = lttbDecimation(data, start, count, availableWidth, options);\n break;\n case 'min-max':\n decimated = minMaxDecimation(data, start, count, availableWidth);\n break;\n default:\n throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`);\n }\n\n dataset._decimated = decimated;\n });\n },\n\n destroy(chart) {\n cleanDecimatedData(chart);\n }\n};\n","import {_boundSegment, _boundSegments, _normalizeAngle} from '../../helpers/index.js';\n\nexport function _segments(line, target, property) {\n const segments = line.segments;\n const points = line.points;\n const tpoints = target.points;\n const parts = [];\n\n for (const segment of segments) {\n let {start, end} = segment;\n end = _findSegmentEnd(start, end, points);\n\n const bounds = _getBounds(property, points[start], points[end], segment.loop);\n\n if (!target.segments) {\n // Special case for boundary not supporting `segments` (simpleArc)\n // Bounds are provided as `target` for partial circle, or undefined for full circle\n parts.push({\n source: segment,\n target: bounds,\n start: points[start],\n end: points[end]\n });\n continue;\n }\n\n // Get all segments from `target` that intersect the bounds of current segment of `line`\n const targetSegments = _boundSegments(target, bounds);\n\n for (const tgt of targetSegments) {\n const subBounds = _getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop);\n const fillSources = _boundSegment(segment, points, subBounds);\n\n for (const fillSource of fillSources) {\n parts.push({\n source: fillSource,\n target: tgt,\n start: {\n [property]: _getEdge(bounds, subBounds, 'start', Math.max)\n },\n end: {\n [property]: _getEdge(bounds, subBounds, 'end', Math.min)\n }\n });\n }\n }\n }\n return parts;\n}\n\nexport function _getBounds(property, first, last, loop) {\n if (loop) {\n return;\n }\n let start = first[property];\n let end = last[property];\n\n if (property === 'angle') {\n start = _normalizeAngle(start);\n end = _normalizeAngle(end);\n }\n return {property, start, end};\n}\n\nexport function _pointsFromSegments(boundary, line) {\n const {x = null, y = null} = boundary || {};\n const linePoints = line.points;\n const points = [];\n line.segments.forEach(({start, end}) => {\n end = _findSegmentEnd(start, end, linePoints);\n const first = linePoints[start];\n const last = linePoints[end];\n if (y !== null) {\n points.push({x: first.x, y});\n points.push({x: last.x, y});\n } else if (x !== null) {\n points.push({x, y: first.y});\n points.push({x, y: last.y});\n }\n });\n return points;\n}\n\nexport function _findSegmentEnd(start, end, points) {\n for (;end > start; end--) {\n const point = points[end];\n if (!isNaN(point.x) && !isNaN(point.y)) {\n break;\n }\n }\n return end;\n}\n\nfunction _getEdge(a, b, prop, fn) {\n if (a && b) {\n return fn(a[prop], b[prop]);\n }\n return a ? a[prop] : b ? b[prop] : 0;\n}\n","/**\n * @typedef { import('../../core/core.controller.js').default } Chart\n * @typedef { import('../../core/core.scale.js').default } Scale\n * @typedef { import('../../elements/element.point.js').default } PointElement\n */\n\nimport {LineElement} from '../../elements/index.js';\nimport {isArray} from '../../helpers/index.js';\nimport {_pointsFromSegments} from './filler.segment.js';\n\n/**\n * @param {PointElement[] | { x: number; y: number; }} boundary\n * @param {LineElement} line\n * @return {LineElement?}\n */\nexport function _createBoundaryLine(boundary, line) {\n let points = [];\n let _loop = false;\n\n if (isArray(boundary)) {\n _loop = true;\n // @ts-ignore\n points = boundary;\n } else {\n points = _pointsFromSegments(boundary, line);\n }\n\n return points.length ? new LineElement({\n points,\n options: {tension: 0},\n _loop,\n _fullLoop: _loop\n }) : null;\n}\n\nexport function _shouldApplyFill(source) {\n return source && source.fill !== false;\n}\n","import {isObject, isFinite, valueOrDefault} from '../../helpers/helpers.core.js';\n\n/**\n * @typedef { import('../../core/core.scale.js').default } Scale\n * @typedef { import('../../elements/element.line.js').default } LineElement\n * @typedef { import('../../types/index.js').FillTarget } FillTarget\n * @typedef { import('../../types/index.js').ComplexFillTarget } ComplexFillTarget\n */\n\nexport function _resolveTarget(sources, index, propagate) {\n const source = sources[index];\n let fill = source.fill;\n const visited = [index];\n let target;\n\n if (!propagate) {\n return fill;\n }\n\n while (fill !== false && visited.indexOf(fill) === -1) {\n if (!isFinite(fill)) {\n return fill;\n }\n\n target = sources[fill];\n if (!target) {\n return false;\n }\n\n if (target.visible) {\n return fill;\n }\n\n visited.push(fill);\n fill = target.fill;\n }\n\n return false;\n}\n\n/**\n * @param {LineElement} line\n * @param {number} index\n * @param {number} count\n */\nexport function _decodeFill(line, index, count) {\n /** @type {string | {value: number}} */\n const fill = parseFillOption(line);\n\n if (isObject(fill)) {\n return isNaN(fill.value) ? false : fill;\n }\n\n let target = parseFloat(fill);\n\n if (isFinite(target) && Math.floor(target) === target) {\n return decodeTargetIndex(fill[0], index, target, count);\n }\n\n return ['origin', 'start', 'end', 'stack', 'shape'].indexOf(fill) >= 0 && fill;\n}\n\nfunction decodeTargetIndex(firstCh, index, target, count) {\n if (firstCh === '-' || firstCh === '+') {\n target = index + target;\n }\n\n if (target === index || target < 0 || target >= count) {\n return false;\n }\n\n return target;\n}\n\n/**\n * @param {FillTarget | ComplexFillTarget} fill\n * @param {Scale} scale\n * @returns {number | null}\n */\nexport function _getTargetPixel(fill, scale) {\n let pixel = null;\n if (fill === 'start') {\n pixel = scale.bottom;\n } else if (fill === 'end') {\n pixel = scale.top;\n } else if (isObject(fill)) {\n // @ts-ignore\n pixel = scale.getPixelForValue(fill.value);\n } else if (scale.getBasePixel) {\n pixel = scale.getBasePixel();\n }\n return pixel;\n}\n\n/**\n * @param {FillTarget | ComplexFillTarget} fill\n * @param {Scale} scale\n * @param {number} startValue\n * @returns {number | undefined}\n */\nexport function _getTargetValue(fill, scale, startValue) {\n let value;\n\n if (fill === 'start') {\n value = startValue;\n } else if (fill === 'end') {\n value = scale.options.reverse ? scale.min : scale.max;\n } else if (isObject(fill)) {\n // @ts-ignore\n value = fill.value;\n } else {\n value = scale.getBaseValue();\n }\n return value;\n}\n\n/**\n * @param {LineElement} line\n */\nfunction parseFillOption(line) {\n const options = line.options;\n const fillOption = options.fill;\n let fill = valueOrDefault(fillOption && fillOption.target, fillOption);\n\n if (fill === undefined) {\n fill = !!options.backgroundColor;\n }\n\n if (fill === false || fill === null) {\n return false;\n }\n\n if (fill === true) {\n return 'origin';\n }\n return fill;\n}\n","/**\n * @typedef { import('../../core/core.controller.js').default } Chart\n * @typedef { import('../../core/core.scale.js').default } Scale\n * @typedef { import('../../elements/element.point.js').default } PointElement\n */\n\nimport {LineElement} from '../../elements/index.js';\nimport {_isBetween} from '../../helpers/index.js';\nimport {_createBoundaryLine} from './filler.helper.js';\n\n/**\n * @param {{ chart: Chart; scale: Scale; index: number; line: LineElement; }} source\n * @return {LineElement}\n */\nexport function _buildStackLine(source) {\n const {scale, index, line} = source;\n const points = [];\n const segments = line.segments;\n const sourcePoints = line.points;\n const linesBelow = getLinesBelow(scale, index);\n linesBelow.push(_createBoundaryLine({x: null, y: scale.bottom}, line));\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n for (let j = segment.start; j <= segment.end; j++) {\n addPointsBelow(points, sourcePoints[j], linesBelow);\n }\n }\n return new LineElement({points, options: {}});\n}\n\n/**\n * @param {Scale} scale\n * @param {number} index\n * @return {LineElement[]}\n */\nfunction getLinesBelow(scale, index) {\n const below = [];\n const metas = scale.getMatchingVisibleMetas('line');\n\n for (let i = 0; i < metas.length; i++) {\n const meta = metas[i];\n if (meta.index === index) {\n break;\n }\n if (!meta.hidden) {\n below.unshift(meta.dataset);\n }\n }\n return below;\n}\n\n/**\n * @param {PointElement[]} points\n * @param {PointElement} sourcePoint\n * @param {LineElement[]} linesBelow\n */\nfunction addPointsBelow(points, sourcePoint, linesBelow) {\n const postponed = [];\n for (let j = 0; j < linesBelow.length; j++) {\n const line = linesBelow[j];\n const {first, last, point} = findPoint(line, sourcePoint, 'x');\n\n if (!point || (first && last)) {\n continue;\n }\n if (first) {\n // First point of an segment -> need to add another point before this,\n // from next line below.\n postponed.unshift(point);\n } else {\n points.push(point);\n if (!last) {\n // In the middle of an segment, no need to add more points.\n break;\n }\n }\n }\n points.push(...postponed);\n}\n\n/**\n * @param {LineElement} line\n * @param {PointElement} sourcePoint\n * @param {string} property\n * @returns {{point?: PointElement, first?: boolean, last?: boolean}}\n */\nfunction findPoint(line, sourcePoint, property) {\n const point = line.interpolate(sourcePoint, property);\n if (!point) {\n return {};\n }\n\n const pointValue = point[property];\n const segments = line.segments;\n const linePoints = line.points;\n let first = false;\n let last = false;\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const firstValue = linePoints[segment.start][property];\n const lastValue = linePoints[segment.end][property];\n if (_isBetween(pointValue, firstValue, lastValue)) {\n first = pointValue === firstValue;\n last = pointValue === lastValue;\n break;\n }\n }\n return {first, last, point};\n}\n","import {TAU} from '../../helpers/index.js';\n\n// TODO: use elements.ArcElement instead\nexport class simpleArc {\n constructor(opts) {\n this.x = opts.x;\n this.y = opts.y;\n this.radius = opts.radius;\n }\n\n pathSegment(ctx, bounds, opts) {\n const {x, y, radius} = this;\n bounds = bounds || {start: 0, end: TAU};\n ctx.arc(x, y, radius, bounds.end, bounds.start, true);\n return !opts.bounds;\n }\n\n interpolate(point) {\n const {x, y, radius} = this;\n const angle = point.angle;\n return {\n x: x + Math.cos(angle) * radius,\n y: y + Math.sin(angle) * radius,\n angle\n };\n }\n}\n","import {isFinite} from '../../helpers/index.js';\nimport {_createBoundaryLine} from './filler.helper.js';\nimport {_getTargetPixel, _getTargetValue} from './filler.options.js';\nimport {_buildStackLine} from './filler.target.stack.js';\nimport {simpleArc} from './simpleArc.js';\n\n/**\n * @typedef { import('../../core/core.controller.js').default } Chart\n * @typedef { import('../../core/core.scale.js').default } Scale\n * @typedef { import('../../elements/element.point.js').default } PointElement\n */\n\nexport function _getTarget(source) {\n const {chart, fill, line} = source;\n\n if (isFinite(fill)) {\n return getLineByIndex(chart, fill);\n }\n\n if (fill === 'stack') {\n return _buildStackLine(source);\n }\n\n if (fill === 'shape') {\n return true;\n }\n\n const boundary = computeBoundary(source);\n\n if (boundary instanceof simpleArc) {\n return boundary;\n }\n\n return _createBoundaryLine(boundary, line);\n}\n\n/**\n * @param {Chart} chart\n * @param {number} index\n */\nfunction getLineByIndex(chart, index) {\n const meta = chart.getDatasetMeta(index);\n const visible = meta && chart.isDatasetVisible(index);\n return visible ? meta.dataset : null;\n}\n\nfunction computeBoundary(source) {\n const scale = source.scale || {};\n\n if (scale.getPointPositionForValue) {\n return computeCircularBoundary(source);\n }\n return computeLinearBoundary(source);\n}\n\n\nfunction computeLinearBoundary(source) {\n const {scale = {}, fill} = source;\n const pixel = _getTargetPixel(fill, scale);\n\n if (isFinite(pixel)) {\n const horizontal = scale.isHorizontal();\n\n return {\n x: horizontal ? pixel : null,\n y: horizontal ? null : pixel\n };\n }\n\n return null;\n}\n\nfunction computeCircularBoundary(source) {\n const {scale, fill} = source;\n const options = scale.options;\n const length = scale.getLabels().length;\n const start = options.reverse ? scale.max : scale.min;\n const value = _getTargetValue(fill, scale, start);\n const target = [];\n\n if (options.grid.circular) {\n const center = scale.getPointPositionForValue(0, start);\n return new simpleArc({\n x: center.x,\n y: center.y,\n radius: scale.getDistanceFromCenterForValue(value)\n });\n }\n\n for (let i = 0; i < length; ++i) {\n target.push(scale.getPointPositionForValue(i, value));\n }\n return target;\n}\n\n","import {clipArea, unclipArea} from '../../helpers/index.js';\nimport {_findSegmentEnd, _getBounds, _segments} from './filler.segment.js';\nimport {_getTarget} from './filler.target.js';\n\nexport function _drawfill(ctx, source, area) {\n const target = _getTarget(source);\n const {line, scale, axis} = source;\n const lineOpts = line.options;\n const fillOption = lineOpts.fill;\n const color = lineOpts.backgroundColor;\n const {above = color, below = color} = fillOption || {};\n if (target && line.points.length) {\n clipArea(ctx, area);\n doFill(ctx, {line, target, above, below, area, scale, axis});\n unclipArea(ctx);\n }\n}\n\nfunction doFill(ctx, cfg) {\n const {line, target, above, below, area, scale} = cfg;\n const property = line._loop ? 'angle' : cfg.axis;\n\n ctx.save();\n\n if (property === 'x' && below !== above) {\n clipVertical(ctx, target, area.top);\n fill(ctx, {line, target, color: above, scale, property});\n ctx.restore();\n ctx.save();\n clipVertical(ctx, target, area.bottom);\n }\n fill(ctx, {line, target, color: below, scale, property});\n\n ctx.restore();\n}\n\nfunction clipVertical(ctx, target, clipY) {\n const {segments, points} = target;\n let first = true;\n let lineLoop = false;\n\n ctx.beginPath();\n for (const segment of segments) {\n const {start, end} = segment;\n const firstPoint = points[start];\n const lastPoint = points[_findSegmentEnd(start, end, points)];\n if (first) {\n ctx.moveTo(firstPoint.x, firstPoint.y);\n first = false;\n } else {\n ctx.lineTo(firstPoint.x, clipY);\n ctx.lineTo(firstPoint.x, firstPoint.y);\n }\n lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop});\n if (lineLoop) {\n ctx.closePath();\n } else {\n ctx.lineTo(lastPoint.x, clipY);\n }\n }\n\n ctx.lineTo(target.first().x, clipY);\n ctx.closePath();\n ctx.clip();\n}\n\nfunction fill(ctx, cfg) {\n const {line, target, property, color, scale} = cfg;\n const segments = _segments(line, target, property);\n\n for (const {source: src, target: tgt, start, end} of segments) {\n const {style: {backgroundColor = color} = {}} = src;\n const notShape = target !== true;\n\n ctx.save();\n ctx.fillStyle = backgroundColor;\n\n clipBounds(ctx, scale, notShape && _getBounds(property, start, end));\n\n ctx.beginPath();\n\n const lineLoop = !!line.pathSegment(ctx, src);\n\n let loop;\n if (notShape) {\n if (lineLoop) {\n ctx.closePath();\n } else {\n interpolatedLineTo(ctx, target, end, property);\n }\n\n const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true});\n loop = lineLoop && targetLoop;\n if (!loop) {\n interpolatedLineTo(ctx, target, start, property);\n }\n }\n\n ctx.closePath();\n ctx.fill(loop ? 'evenodd' : 'nonzero');\n\n ctx.restore();\n }\n}\n\nfunction clipBounds(ctx, scale, bounds) {\n const {top, bottom} = scale.chart.chartArea;\n const {property, start, end} = bounds || {};\n if (property === 'x') {\n ctx.beginPath();\n ctx.rect(start, top, end - start, bottom - top);\n ctx.clip();\n }\n}\n\nfunction interpolatedLineTo(ctx, target, point, property) {\n const interpolatedPoint = target.interpolate(point, property);\n if (interpolatedPoint) {\n ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y);\n }\n}\n\n","/**\n * Plugin based on discussion from the following Chart.js issues:\n * @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569\n * @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897\n */\n\nimport LineElement from '../../elements/element.line.js';\nimport {_drawfill} from './filler.drawing.js';\nimport {_shouldApplyFill} from './filler.helper.js';\nimport {_decodeFill, _resolveTarget} from './filler.options.js';\n\nexport default {\n id: 'filler',\n\n afterDatasetsUpdate(chart, _args, options) {\n const count = (chart.data.datasets || []).length;\n const sources = [];\n let meta, i, line, source;\n\n for (i = 0; i < count; ++i) {\n meta = chart.getDatasetMeta(i);\n line = meta.dataset;\n source = null;\n\n if (line && line.options && line instanceof LineElement) {\n source = {\n visible: chart.isDatasetVisible(i),\n index: i,\n fill: _decodeFill(line, i, count),\n chart,\n axis: meta.controller.options.indexAxis,\n scale: meta.vScale,\n line,\n };\n }\n\n meta.$filler = source;\n sources.push(source);\n }\n\n for (i = 0; i < count; ++i) {\n source = sources[i];\n if (!source || source.fill === false) {\n continue;\n }\n\n source.fill = _resolveTarget(sources, i, options.propagate);\n }\n },\n\n beforeDraw(chart, _args, options) {\n const draw = options.drawTime === 'beforeDraw';\n const metasets = chart.getSortedVisibleDatasetMetas();\n const area = chart.chartArea;\n for (let i = metasets.length - 1; i >= 0; --i) {\n const source = metasets[i].$filler;\n if (!source) {\n continue;\n }\n\n source.line.updateControlPoints(area, source.axis);\n if (draw && source.fill) {\n _drawfill(chart.ctx, source, area);\n }\n }\n },\n\n beforeDatasetsDraw(chart, _args, options) {\n if (options.drawTime !== 'beforeDatasetsDraw') {\n return;\n }\n\n const metasets = chart.getSortedVisibleDatasetMetas();\n for (let i = metasets.length - 1; i >= 0; --i) {\n const source = metasets[i].$filler;\n\n if (_shouldApplyFill(source)) {\n _drawfill(chart.ctx, source, chart.chartArea);\n }\n }\n },\n\n beforeDatasetDraw(chart, args, options) {\n const source = args.meta.$filler;\n\n if (!_shouldApplyFill(source) || options.drawTime !== 'beforeDatasetDraw') {\n return;\n }\n\n _drawfill(chart.ctx, source, chart.chartArea);\n },\n\n defaults: {\n propagate: true,\n drawTime: 'beforeDatasetDraw'\n }\n};\n","import defaults from '../core/core.defaults.js';\nimport Element from '../core/core.element.js';\nimport layouts from '../core/core.layouts.js';\nimport {addRoundedRectPath, drawPointLegend, renderText} from '../helpers/helpers.canvas.js';\nimport {\n _isBetween,\n callback as call,\n clipArea,\n getRtlAdapter,\n overrideTextDirection,\n restoreTextDirection,\n toFont,\n toPadding,\n unclipArea,\n valueOrDefault,\n} from '../helpers/index.js';\nimport {_alignStartEnd, _textX, _toLeftRightCenter} from '../helpers/helpers.extras.js';\nimport {toTRBLCorners} from '../helpers/helpers.options.js';\n\n/**\n * @typedef { import('../types/index.js').ChartEvent } ChartEvent\n */\n\nconst getBoxSize = (labelOpts, fontSize) => {\n let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts;\n\n if (labelOpts.usePointStyle) {\n boxHeight = Math.min(boxHeight, fontSize);\n boxWidth = labelOpts.pointStyleWidth || Math.min(boxWidth, fontSize);\n }\n\n return {\n boxWidth,\n boxHeight,\n itemHeight: Math.max(fontSize, boxHeight)\n };\n};\n\nconst itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index;\n\nexport class Legend extends Element {\n\n /**\n\t * @param {{ ctx: any; options: any; chart: any; }} config\n\t */\n constructor(config) {\n super();\n\n this._added = false;\n\n // Contains hit boxes for each dataset (in dataset order)\n this.legendHitBoxes = [];\n\n /**\n \t\t * @private\n \t\t */\n this._hoveredItem = null;\n\n // Are we in doughnut mode which has a different data type\n this.doughnutMode = false;\n\n this.chart = config.chart;\n this.options = config.options;\n this.ctx = config.ctx;\n this.legendItems = undefined;\n this.columnSizes = undefined;\n this.lineWidths = undefined;\n this.maxHeight = undefined;\n this.maxWidth = undefined;\n this.top = undefined;\n this.bottom = undefined;\n this.left = undefined;\n this.right = undefined;\n this.height = undefined;\n this.width = undefined;\n this._margins = undefined;\n this.position = undefined;\n this.weight = undefined;\n this.fullSize = undefined;\n }\n\n update(maxWidth, maxHeight, margins) {\n this.maxWidth = maxWidth;\n this.maxHeight = maxHeight;\n this._margins = margins;\n\n this.setDimensions();\n this.buildLabels();\n this.fit();\n }\n\n setDimensions() {\n if (this.isHorizontal()) {\n this.width = this.maxWidth;\n this.left = this._margins.left;\n this.right = this.width;\n } else {\n this.height = this.maxHeight;\n this.top = this._margins.top;\n this.bottom = this.height;\n }\n }\n\n buildLabels() {\n const labelOpts = this.options.labels || {};\n let legendItems = call(labelOpts.generateLabels, [this.chart], this) || [];\n\n if (labelOpts.filter) {\n legendItems = legendItems.filter((item) => labelOpts.filter(item, this.chart.data));\n }\n\n if (labelOpts.sort) {\n legendItems = legendItems.sort((a, b) => labelOpts.sort(a, b, this.chart.data));\n }\n\n if (this.options.reverse) {\n legendItems.reverse();\n }\n\n this.legendItems = legendItems;\n }\n\n fit() {\n const {options, ctx} = this;\n\n // The legend may not be displayed for a variety of reasons including\n // the fact that the defaults got set to `false`.\n // When the legend is not displayed, there are no guarantees that the options\n // are correctly formatted so we need to bail out as early as possible.\n if (!options.display) {\n this.width = this.height = 0;\n return;\n }\n\n const labelOpts = options.labels;\n const labelFont = toFont(labelOpts.font);\n const fontSize = labelFont.size;\n const titleHeight = this._computeTitleHeight();\n const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize);\n\n let width, height;\n\n ctx.font = labelFont.string;\n\n if (this.isHorizontal()) {\n width = this.maxWidth; // fill all the width\n height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10;\n } else {\n height = this.maxHeight; // fill all the height\n width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10;\n }\n\n this.width = Math.min(width, options.maxWidth || this.maxWidth);\n this.height = Math.min(height, options.maxHeight || this.maxHeight);\n }\n\n /**\n\t * @private\n\t */\n _fitRows(titleHeight, fontSize, boxWidth, itemHeight) {\n const {ctx, maxWidth, options: {labels: {padding}}} = this;\n const hitboxes = this.legendHitBoxes = [];\n // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one\n const lineWidths = this.lineWidths = [0];\n const lineHeight = itemHeight + padding;\n let totalHeight = titleHeight;\n\n ctx.textAlign = 'left';\n ctx.textBaseline = 'middle';\n\n let row = -1;\n let top = -lineHeight;\n this.legendItems.forEach((legendItem, i) => {\n const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width;\n\n if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) {\n totalHeight += lineHeight;\n lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0;\n top += lineHeight;\n row++;\n }\n\n hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight};\n\n lineWidths[lineWidths.length - 1] += itemWidth + padding;\n });\n\n return totalHeight;\n }\n\n _fitCols(titleHeight, labelFont, boxWidth, _itemHeight) {\n const {ctx, maxHeight, options: {labels: {padding}}} = this;\n const hitboxes = this.legendHitBoxes = [];\n const columnSizes = this.columnSizes = [];\n const heightLimit = maxHeight - titleHeight;\n\n let totalWidth = padding;\n let currentColWidth = 0;\n let currentColHeight = 0;\n\n let left = 0;\n let col = 0;\n\n this.legendItems.forEach((legendItem, i) => {\n const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight);\n\n // If too tall, go to new column\n if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) {\n totalWidth += currentColWidth + padding;\n columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size\n left += currentColWidth + padding;\n col++;\n currentColWidth = currentColHeight = 0;\n }\n\n // Store the hitbox width and height here. Final position will be updated in `draw`\n hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight};\n\n // Get max width\n currentColWidth = Math.max(currentColWidth, itemWidth);\n currentColHeight += itemHeight + padding;\n });\n\n totalWidth += currentColWidth;\n columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size\n\n return totalWidth;\n }\n\n adjustHitBoxes() {\n if (!this.options.display) {\n return;\n }\n const titleHeight = this._computeTitleHeight();\n const {legendHitBoxes: hitboxes, options: {align, labels: {padding}, rtl}} = this;\n const rtlHelper = getRtlAdapter(rtl, this.left, this.width);\n if (this.isHorizontal()) {\n let row = 0;\n let left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]);\n for (const hitbox of hitboxes) {\n if (row !== hitbox.row) {\n row = hitbox.row;\n left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]);\n }\n hitbox.top += this.top + titleHeight + padding;\n hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(left), hitbox.width);\n left += hitbox.width + padding;\n }\n } else {\n let col = 0;\n let top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height);\n for (const hitbox of hitboxes) {\n if (hitbox.col !== col) {\n col = hitbox.col;\n top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height);\n }\n hitbox.top = top;\n hitbox.left += this.left + padding;\n hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(hitbox.left), hitbox.width);\n top += hitbox.height + padding;\n }\n }\n }\n\n isHorizontal() {\n return this.options.position === 'top' || this.options.position === 'bottom';\n }\n\n draw() {\n if (this.options.display) {\n const ctx = this.ctx;\n clipArea(ctx, this);\n\n this._draw();\n\n unclipArea(ctx);\n }\n }\n\n /**\n\t * @private\n\t */\n _draw() {\n const {options: opts, columnSizes, lineWidths, ctx} = this;\n const {align, labels: labelOpts} = opts;\n const defaultColor = defaults.color;\n const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width);\n const labelFont = toFont(labelOpts.font);\n const {padding} = labelOpts;\n const fontSize = labelFont.size;\n const halfFontSize = fontSize / 2;\n let cursor;\n\n this.drawTitle();\n\n // Canvas setup\n ctx.textAlign = rtlHelper.textAlign('left');\n ctx.textBaseline = 'middle';\n ctx.lineWidth = 0.5;\n ctx.font = labelFont.string;\n\n const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize);\n\n // current position\n const drawLegendBox = function(x, y, legendItem) {\n if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) {\n return;\n }\n\n // Set the ctx for the box\n ctx.save();\n\n const lineWidth = valueOrDefault(legendItem.lineWidth, 1);\n ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor);\n ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt');\n ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0);\n ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter');\n ctx.lineWidth = lineWidth;\n ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor);\n\n ctx.setLineDash(valueOrDefault(legendItem.lineDash, []));\n\n if (labelOpts.usePointStyle) {\n // Recalculate x and y for drawPoint() because its expecting\n // x and y to be center of figure (instead of top left)\n const drawOptions = {\n radius: boxHeight * Math.SQRT2 / 2,\n pointStyle: legendItem.pointStyle,\n rotation: legendItem.rotation,\n borderWidth: lineWidth\n };\n const centerX = rtlHelper.xPlus(x, boxWidth / 2);\n const centerY = y + halfFontSize;\n\n // Draw pointStyle as legend symbol\n drawPointLegend(ctx, drawOptions, centerX, centerY, labelOpts.pointStyleWidth && boxWidth);\n } else {\n // Draw box as legend symbol\n // Adjust position when boxHeight < fontSize (want it centered)\n const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0);\n const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth);\n const borderRadius = toTRBLCorners(legendItem.borderRadius);\n\n ctx.beginPath();\n\n if (Object.values(borderRadius).some(v => v !== 0)) {\n addRoundedRectPath(ctx, {\n x: xBoxLeft,\n y: yBoxTop,\n w: boxWidth,\n h: boxHeight,\n radius: borderRadius,\n });\n } else {\n ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight);\n }\n\n ctx.fill();\n if (lineWidth !== 0) {\n ctx.stroke();\n }\n }\n\n ctx.restore();\n };\n\n const fillText = function(x, y, legendItem) {\n renderText(ctx, legendItem.text, x, y + (itemHeight / 2), labelFont, {\n strikethrough: legendItem.hidden,\n textAlign: rtlHelper.textAlign(legendItem.textAlign)\n });\n };\n\n // Horizontal\n const isHorizontal = this.isHorizontal();\n const titleHeight = this._computeTitleHeight();\n if (isHorizontal) {\n cursor = {\n x: _alignStartEnd(align, this.left + padding, this.right - lineWidths[0]),\n y: this.top + padding + titleHeight,\n line: 0\n };\n } else {\n cursor = {\n x: this.left + padding,\n y: _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[0].height),\n line: 0\n };\n }\n\n overrideTextDirection(this.ctx, opts.textDirection);\n\n const lineHeight = itemHeight + padding;\n this.legendItems.forEach((legendItem, i) => {\n ctx.strokeStyle = legendItem.fontColor; // for strikethrough effect\n ctx.fillStyle = legendItem.fontColor; // render in correct colour\n\n const textWidth = ctx.measureText(legendItem.text).width;\n const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign));\n const width = boxWidth + halfFontSize + textWidth;\n let x = cursor.x;\n let y = cursor.y;\n\n rtlHelper.setWidth(this.width);\n\n if (isHorizontal) {\n if (i > 0 && x + width + padding > this.right) {\n y = cursor.y += lineHeight;\n cursor.line++;\n x = cursor.x = _alignStartEnd(align, this.left + padding, this.right - lineWidths[cursor.line]);\n }\n } else if (i > 0 && y + lineHeight > this.bottom) {\n x = cursor.x = x + columnSizes[cursor.line].width + padding;\n cursor.line++;\n y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[cursor.line].height);\n }\n\n const realX = rtlHelper.x(x);\n\n drawLegendBox(realX, y, legendItem);\n\n x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : this.right, opts.rtl);\n\n // Fill the actual label\n fillText(rtlHelper.x(x), y, legendItem);\n\n if (isHorizontal) {\n cursor.x += width + padding;\n } else if (typeof legendItem.text !== 'string') {\n const fontLineHeight = labelFont.lineHeight;\n cursor.y += calculateLegendItemHeight(legendItem, fontLineHeight) + padding;\n } else {\n cursor.y += lineHeight;\n }\n });\n\n restoreTextDirection(this.ctx, opts.textDirection);\n }\n\n /**\n\t * @protected\n\t */\n drawTitle() {\n const opts = this.options;\n const titleOpts = opts.title;\n const titleFont = toFont(titleOpts.font);\n const titlePadding = toPadding(titleOpts.padding);\n\n if (!titleOpts.display) {\n return;\n }\n\n const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width);\n const ctx = this.ctx;\n const position = titleOpts.position;\n const halfFontSize = titleFont.size / 2;\n const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize;\n let y;\n\n // These defaults are used when the legend is vertical.\n // When horizontal, they are computed below.\n let left = this.left;\n let maxWidth = this.width;\n\n if (this.isHorizontal()) {\n // Move left / right so that the title is above the legend lines\n maxWidth = Math.max(...this.lineWidths);\n y = this.top + topPaddingPlusHalfFontSize;\n left = _alignStartEnd(opts.align, left, this.right - maxWidth);\n } else {\n // Move down so that the title is above the legend stack in every alignment\n const maxHeight = this.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0);\n y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, this.top, this.bottom - maxHeight - opts.labels.padding - this._computeTitleHeight());\n }\n\n // Now that we know the left edge of the inner legend box, compute the correct\n // X coordinate from the title alignment\n const x = _alignStartEnd(position, left, left + maxWidth);\n\n // Canvas setup\n ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position));\n ctx.textBaseline = 'middle';\n ctx.strokeStyle = titleOpts.color;\n ctx.fillStyle = titleOpts.color;\n ctx.font = titleFont.string;\n\n renderText(ctx, titleOpts.text, x, y, titleFont);\n }\n\n /**\n\t * @private\n\t */\n _computeTitleHeight() {\n const titleOpts = this.options.title;\n const titleFont = toFont(titleOpts.font);\n const titlePadding = toPadding(titleOpts.padding);\n return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0;\n }\n\n /**\n\t * @private\n\t */\n _getLegendItemAt(x, y) {\n let i, hitBox, lh;\n\n if (_isBetween(x, this.left, this.right)\n && _isBetween(y, this.top, this.bottom)) {\n // See if we are touching one of the dataset boxes\n lh = this.legendHitBoxes;\n for (i = 0; i < lh.length; ++i) {\n hitBox = lh[i];\n\n if (_isBetween(x, hitBox.left, hitBox.left + hitBox.width)\n && _isBetween(y, hitBox.top, hitBox.top + hitBox.height)) {\n // Touching an element\n return this.legendItems[i];\n }\n }\n }\n\n return null;\n }\n\n /**\n\t * Handle an event\n\t * @param {ChartEvent} e - The event to handle\n\t */\n handleEvent(e) {\n const opts = this.options;\n if (!isListened(e.type, opts)) {\n return;\n }\n\n // Chart event already has relative position in it\n const hoveredItem = this._getLegendItemAt(e.x, e.y);\n\n if (e.type === 'mousemove' || e.type === 'mouseout') {\n const previous = this._hoveredItem;\n const sameItem = itemsEqual(previous, hoveredItem);\n if (previous && !sameItem) {\n call(opts.onLeave, [e, previous, this], this);\n }\n\n this._hoveredItem = hoveredItem;\n\n if (hoveredItem && !sameItem) {\n call(opts.onHover, [e, hoveredItem, this], this);\n }\n } else if (hoveredItem) {\n call(opts.onClick, [e, hoveredItem, this], this);\n }\n }\n}\n\nfunction calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) {\n const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx);\n const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight);\n return {itemWidth, itemHeight};\n}\n\nfunction calculateItemWidth(legendItem, boxWidth, labelFont, ctx) {\n let legendItemText = legendItem.text;\n if (legendItemText && typeof legendItemText !== 'string') {\n legendItemText = legendItemText.reduce((a, b) => a.length > b.length ? a : b);\n }\n return boxWidth + (labelFont.size / 2) + ctx.measureText(legendItemText).width;\n}\n\nfunction calculateItemHeight(_itemHeight, legendItem, fontLineHeight) {\n let itemHeight = _itemHeight;\n if (typeof legendItem.text !== 'string') {\n itemHeight = calculateLegendItemHeight(legendItem, fontLineHeight);\n }\n return itemHeight;\n}\n\nfunction calculateLegendItemHeight(legendItem, fontLineHeight) {\n const labelHeight = legendItem.text ? legendItem.text.length : 0;\n return fontLineHeight * labelHeight;\n}\n\nfunction isListened(type, opts) {\n if ((type === 'mousemove' || type === 'mouseout') && (opts.onHover || opts.onLeave)) {\n return true;\n }\n if (opts.onClick && (type === 'click' || type === 'mouseup')) {\n return true;\n }\n return false;\n}\n\nexport default {\n id: 'legend',\n\n /**\n\t * For tests\n\t * @private\n\t */\n _element: Legend,\n\n start(chart, _args, options) {\n const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart});\n layouts.configure(chart, legend, options);\n layouts.addBox(chart, legend);\n },\n\n stop(chart) {\n layouts.removeBox(chart, chart.legend);\n delete chart.legend;\n },\n\n // During the beforeUpdate step, the layout configuration needs to run\n // This ensures that if the legend position changes (via an option update)\n // the layout system respects the change. See https://github.com/chartjs/Chart.js/issues/7527\n beforeUpdate(chart, _args, options) {\n const legend = chart.legend;\n layouts.configure(chart, legend, options);\n legend.options = options;\n },\n\n // The labels need to be built after datasets are updated to ensure that colors\n // and other styling are correct. See https://github.com/chartjs/Chart.js/issues/6968\n afterUpdate(chart) {\n const legend = chart.legend;\n legend.buildLabels();\n legend.adjustHitBoxes();\n },\n\n\n afterEvent(chart, args) {\n if (!args.replay) {\n chart.legend.handleEvent(args.event);\n }\n },\n\n defaults: {\n display: true,\n position: 'top',\n align: 'center',\n fullSize: true,\n reverse: false,\n weight: 1000,\n\n // a callback that will handle\n onClick(e, legendItem, legend) {\n const index = legendItem.datasetIndex;\n const ci = legend.chart;\n if (ci.isDatasetVisible(index)) {\n ci.hide(index);\n legendItem.hidden = true;\n } else {\n ci.show(index);\n legendItem.hidden = false;\n }\n },\n\n onHover: null,\n onLeave: null,\n\n labels: {\n color: (ctx) => ctx.chart.options.color,\n boxWidth: 40,\n padding: 10,\n // Generates labels shown in the legend\n // Valid properties to return:\n // text : text to display\n // fillStyle : fill of coloured box\n // strokeStyle: stroke of coloured box\n // hidden : if this legend item refers to a hidden item\n // lineCap : cap style for line\n // lineDash\n // lineDashOffset :\n // lineJoin :\n // lineWidth :\n generateLabels(chart) {\n const datasets = chart.data.datasets;\n const {labels: {usePointStyle, pointStyle, textAlign, color, useBorderRadius, borderRadius}} = chart.legend.options;\n\n return chart._getSortedDatasetMetas().map((meta) => {\n const style = meta.controller.getStyle(usePointStyle ? 0 : undefined);\n const borderWidth = toPadding(style.borderWidth);\n\n return {\n text: datasets[meta.index].label,\n fillStyle: style.backgroundColor,\n fontColor: color,\n hidden: !meta.visible,\n lineCap: style.borderCapStyle,\n lineDash: style.borderDash,\n lineDashOffset: style.borderDashOffset,\n lineJoin: style.borderJoinStyle,\n lineWidth: (borderWidth.width + borderWidth.height) / 4,\n strokeStyle: style.borderColor,\n pointStyle: pointStyle || style.pointStyle,\n rotation: style.rotation,\n textAlign: textAlign || style.textAlign,\n borderRadius: useBorderRadius && (borderRadius || style.borderRadius),\n\n // Below is extra data used for toggling the datasets\n datasetIndex: meta.index\n };\n }, this);\n }\n },\n\n title: {\n color: (ctx) => ctx.chart.options.color,\n display: false,\n position: 'center',\n text: '',\n }\n },\n\n descriptors: {\n _scriptable: (name) => !name.startsWith('on'),\n labels: {\n _scriptable: (name) => !['generateLabels', 'filter', 'sort'].includes(name),\n }\n },\n};\n","import Element from '../core/core.element.js';\nimport layouts from '../core/core.layouts.js';\nimport {PI, isArray, toPadding, toFont} from '../helpers/index.js';\nimport {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras.js';\nimport {renderText} from '../helpers/helpers.canvas.js';\n\nexport class Title extends Element {\n /**\n\t * @param {{ ctx: any; options: any; chart: any; }} config\n\t */\n constructor(config) {\n super();\n\n this.chart = config.chart;\n this.options = config.options;\n this.ctx = config.ctx;\n this._padding = undefined;\n this.top = undefined;\n this.bottom = undefined;\n this.left = undefined;\n this.right = undefined;\n this.width = undefined;\n this.height = undefined;\n this.position = undefined;\n this.weight = undefined;\n this.fullSize = undefined;\n }\n\n update(maxWidth, maxHeight) {\n const opts = this.options;\n\n this.left = 0;\n this.top = 0;\n\n if (!opts.display) {\n this.width = this.height = this.right = this.bottom = 0;\n return;\n }\n\n this.width = this.right = maxWidth;\n this.height = this.bottom = maxHeight;\n\n const lineCount = isArray(opts.text) ? opts.text.length : 1;\n this._padding = toPadding(opts.padding);\n const textSize = lineCount * toFont(opts.font).lineHeight + this._padding.height;\n\n if (this.isHorizontal()) {\n this.height = textSize;\n } else {\n this.width = textSize;\n }\n }\n\n isHorizontal() {\n const pos = this.options.position;\n return pos === 'top' || pos === 'bottom';\n }\n\n _drawArgs(offset) {\n const {top, left, bottom, right, options} = this;\n const align = options.align;\n let rotation = 0;\n let maxWidth, titleX, titleY;\n\n if (this.isHorizontal()) {\n titleX = _alignStartEnd(align, left, right);\n titleY = top + offset;\n maxWidth = right - left;\n } else {\n if (options.position === 'left') {\n titleX = left + offset;\n titleY = _alignStartEnd(align, bottom, top);\n rotation = PI * -0.5;\n } else {\n titleX = right - offset;\n titleY = _alignStartEnd(align, top, bottom);\n rotation = PI * 0.5;\n }\n maxWidth = bottom - top;\n }\n return {titleX, titleY, maxWidth, rotation};\n }\n\n draw() {\n const ctx = this.ctx;\n const opts = this.options;\n\n if (!opts.display) {\n return;\n }\n\n const fontOpts = toFont(opts.font);\n const lineHeight = fontOpts.lineHeight;\n const offset = lineHeight / 2 + this._padding.top;\n const {titleX, titleY, maxWidth, rotation} = this._drawArgs(offset);\n\n renderText(ctx, opts.text, 0, 0, fontOpts, {\n color: opts.color,\n maxWidth,\n rotation,\n textAlign: _toLeftRightCenter(opts.align),\n textBaseline: 'middle',\n translation: [titleX, titleY],\n });\n }\n}\n\nfunction createTitle(chart, titleOpts) {\n const title = new Title({\n ctx: chart.ctx,\n options: titleOpts,\n chart\n });\n\n layouts.configure(chart, title, titleOpts);\n layouts.addBox(chart, title);\n chart.titleBlock = title;\n}\n\nexport default {\n id: 'title',\n\n /**\n\t * For tests\n\t * @private\n\t */\n _element: Title,\n\n start(chart, _args, options) {\n createTitle(chart, options);\n },\n\n stop(chart) {\n const titleBlock = chart.titleBlock;\n layouts.removeBox(chart, titleBlock);\n delete chart.titleBlock;\n },\n\n beforeUpdate(chart, _args, options) {\n const title = chart.titleBlock;\n layouts.configure(chart, title, options);\n title.options = options;\n },\n\n defaults: {\n align: 'center',\n display: false,\n font: {\n weight: 'bold',\n },\n fullSize: true,\n padding: 10,\n position: 'top',\n text: '',\n weight: 2000 // by default greater than legend (1000) to be above\n },\n\n defaultRoutes: {\n color: 'color'\n },\n\n descriptors: {\n _scriptable: true,\n _indexable: false,\n },\n};\n","import {Title} from './plugin.title.js';\nimport layouts from '../core/core.layouts.js';\n\nconst map = new WeakMap();\n\nexport default {\n id: 'subtitle',\n\n start(chart, _args, options) {\n const title = new Title({\n ctx: chart.ctx,\n options,\n chart\n });\n\n layouts.configure(chart, title, options);\n layouts.addBox(chart, title);\n map.set(chart, title);\n },\n\n stop(chart) {\n layouts.removeBox(chart, map.get(chart));\n map.delete(chart);\n },\n\n beforeUpdate(chart, _args, options) {\n const title = map.get(chart);\n layouts.configure(chart, title, options);\n title.options = options;\n },\n\n defaults: {\n align: 'center',\n display: false,\n font: {\n weight: 'normal',\n },\n fullSize: true,\n padding: 0,\n position: 'top',\n text: '',\n weight: 1500 // by default greater than legend (1000) and smaller than title (2000)\n },\n\n defaultRoutes: {\n color: 'color'\n },\n\n descriptors: {\n _scriptable: true,\n _indexable: false,\n },\n};\n","import Animations from '../core/core.animations.js';\nimport Element from '../core/core.element.js';\nimport {addRoundedRectPath} from '../helpers/helpers.canvas.js';\nimport {each, noop, isNullOrUndef, isArray, _elementsEqual, isObject} from '../helpers/helpers.core.js';\nimport {toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js';\nimport {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl.js';\nimport {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math.js';\nimport {createContext, drawPoint} from '../helpers/index.js';\n\n/**\n * @typedef { import('../platform/platform.base.js').Chart } Chart\n * @typedef { import('../types/index.js').ChartEvent } ChartEvent\n * @typedef { import('../types/index.js').ActiveElement } ActiveElement\n * @typedef { import('../core/core.interaction.js').InteractionItem } InteractionItem\n */\n\nconst positioners = {\n /**\n\t * Average mode places the tooltip at the average position of the elements shown\n\t */\n average(items) {\n if (!items.length) {\n return false;\n }\n\n let i, len;\n let xSet = new Set();\n let y = 0;\n let count = 0;\n\n for (i = 0, len = items.length; i < len; ++i) {\n const el = items[i].element;\n if (el && el.hasValue()) {\n const pos = el.tooltipPosition();\n xSet.add(pos.x);\n y += pos.y;\n ++count;\n }\n }\n\n // No visible items where found, return false so we don't have to divide by 0 which reduces in NaN\n if (count === 0 || xSet.size === 0) {\n return false;\n }\n\n const xAverage = [...xSet].reduce((a, b) => a + b) / xSet.size;\n\n return {\n x: xAverage,\n y: y / count\n };\n },\n\n /**\n\t * Gets the tooltip position nearest of the item nearest to the event position\n\t */\n nearest(items, eventPosition) {\n if (!items.length) {\n return false;\n }\n\n let x = eventPosition.x;\n let y = eventPosition.y;\n let minDistance = Number.POSITIVE_INFINITY;\n let i, len, nearestElement;\n\n for (i = 0, len = items.length; i < len; ++i) {\n const el = items[i].element;\n if (el && el.hasValue()) {\n const center = el.getCenterPoint();\n const d = distanceBetweenPoints(eventPosition, center);\n\n if (d < minDistance) {\n minDistance = d;\n nearestElement = el;\n }\n }\n }\n\n if (nearestElement) {\n const tp = nearestElement.tooltipPosition();\n x = tp.x;\n y = tp.y;\n }\n\n return {\n x,\n y\n };\n }\n};\n\n// Helper to push or concat based on if the 2nd parameter is an array or not\nfunction pushOrConcat(base, toPush) {\n if (toPush) {\n if (isArray(toPush)) {\n // base = base.concat(toPush);\n Array.prototype.push.apply(base, toPush);\n } else {\n base.push(toPush);\n }\n }\n\n return base;\n}\n\n/**\n * Returns array of strings split by newline\n * @param {*} str - The value to split by newline.\n * @returns {string|string[]} value if newline present - Returned from String split() method\n * @function\n */\nfunction splitNewlines(str) {\n if ((typeof str === 'string' || str instanceof String) && str.indexOf('\\n') > -1) {\n return str.split('\\n');\n }\n return str;\n}\n\n\n/**\n * Private helper to create a tooltip item model\n * @param {Chart} chart\n * @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for\n * @return new tooltip item\n */\nfunction createTooltipItem(chart, item) {\n const {element, datasetIndex, index} = item;\n const controller = chart.getDatasetMeta(datasetIndex).controller;\n const {label, value} = controller.getLabelAndValue(index);\n\n return {\n chart,\n label,\n parsed: controller.getParsed(index),\n raw: chart.data.datasets[datasetIndex].data[index],\n formattedValue: value,\n dataset: controller.getDataset(),\n dataIndex: index,\n datasetIndex,\n element\n };\n}\n\n/**\n * Get the size of the tooltip\n */\nfunction getTooltipSize(tooltip, options) {\n const ctx = tooltip.chart.ctx;\n const {body, footer, title} = tooltip;\n const {boxWidth, boxHeight} = options;\n const bodyFont = toFont(options.bodyFont);\n const titleFont = toFont(options.titleFont);\n const footerFont = toFont(options.footerFont);\n const titleLineCount = title.length;\n const footerLineCount = footer.length;\n const bodyLineItemCount = body.length;\n\n const padding = toPadding(options.padding);\n let height = padding.height;\n let width = 0;\n\n // Count of all lines in the body\n let combinedBodyLength = body.reduce((count, bodyItem) => count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0);\n combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length;\n\n if (titleLineCount) {\n height += titleLineCount * titleFont.lineHeight\n\t\t\t+ (titleLineCount - 1) * options.titleSpacing\n\t\t\t+ options.titleMarginBottom;\n }\n if (combinedBodyLength) {\n // Body lines may include some extra height depending on boxHeight\n const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight;\n height += bodyLineItemCount * bodyLineHeight\n\t\t\t+ (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight\n\t\t\t+ (combinedBodyLength - 1) * options.bodySpacing;\n }\n if (footerLineCount) {\n height += options.footerMarginTop\n\t\t\t+ footerLineCount * footerFont.lineHeight\n\t\t\t+ (footerLineCount - 1) * options.footerSpacing;\n }\n\n // Title width\n let widthPadding = 0;\n const maxLineWidth = function(line) {\n width = Math.max(width, ctx.measureText(line).width + widthPadding);\n };\n\n ctx.save();\n\n ctx.font = titleFont.string;\n each(tooltip.title, maxLineWidth);\n\n // Body width\n ctx.font = bodyFont.string;\n each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth);\n\n // Body lines may include some extra width due to the color box\n widthPadding = options.displayColors ? (boxWidth + 2 + options.boxPadding) : 0;\n each(body, (bodyItem) => {\n each(bodyItem.before, maxLineWidth);\n each(bodyItem.lines, maxLineWidth);\n each(bodyItem.after, maxLineWidth);\n });\n\n // Reset back to 0\n widthPadding = 0;\n\n // Footer width\n ctx.font = footerFont.string;\n each(tooltip.footer, maxLineWidth);\n\n ctx.restore();\n\n // Add padding\n width += padding.width;\n\n return {width, height};\n}\n\nfunction determineYAlign(chart, size) {\n const {y, height} = size;\n\n if (y < height / 2) {\n return 'top';\n } else if (y > (chart.height - height / 2)) {\n return 'bottom';\n }\n return 'center';\n}\n\nfunction doesNotFitWithAlign(xAlign, chart, options, size) {\n const {x, width} = size;\n const caret = options.caretSize + options.caretPadding;\n if (xAlign === 'left' && x + width + caret > chart.width) {\n return true;\n }\n\n if (xAlign === 'right' && x - width - caret < 0) {\n return true;\n }\n}\n\nfunction determineXAlign(chart, options, size, yAlign) {\n const {x, width} = size;\n const {width: chartWidth, chartArea: {left, right}} = chart;\n let xAlign = 'center';\n\n if (yAlign === 'center') {\n xAlign = x <= (left + right) / 2 ? 'left' : 'right';\n } else if (x <= width / 2) {\n xAlign = 'left';\n } else if (x >= chartWidth - width / 2) {\n xAlign = 'right';\n }\n\n if (doesNotFitWithAlign(xAlign, chart, options, size)) {\n xAlign = 'center';\n }\n\n return xAlign;\n}\n\n/**\n * Helper to get the alignment of a tooltip given the size\n */\nfunction determineAlignment(chart, options, size) {\n const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size);\n\n return {\n xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign),\n yAlign\n };\n}\n\nfunction alignX(size, xAlign) {\n let {x, width} = size;\n if (xAlign === 'right') {\n x -= width;\n } else if (xAlign === 'center') {\n x -= (width / 2);\n }\n return x;\n}\n\nfunction alignY(size, yAlign, paddingAndSize) {\n // eslint-disable-next-line prefer-const\n let {y, height} = size;\n if (yAlign === 'top') {\n y += paddingAndSize;\n } else if (yAlign === 'bottom') {\n y -= height + paddingAndSize;\n } else {\n y -= (height / 2);\n }\n return y;\n}\n\n/**\n * Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment\n */\nfunction getBackgroundPoint(options, size, alignment, chart) {\n const {caretSize, caretPadding, cornerRadius} = options;\n const {xAlign, yAlign} = alignment;\n const paddingAndSize = caretSize + caretPadding;\n const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius);\n\n let x = alignX(size, xAlign);\n const y = alignY(size, yAlign, paddingAndSize);\n\n if (yAlign === 'center') {\n if (xAlign === 'left') {\n x += paddingAndSize;\n } else if (xAlign === 'right') {\n x -= paddingAndSize;\n }\n } else if (xAlign === 'left') {\n x -= Math.max(topLeft, bottomLeft) + caretSize;\n } else if (xAlign === 'right') {\n x += Math.max(topRight, bottomRight) + caretSize;\n }\n\n return {\n x: _limitValue(x, 0, chart.width - size.width),\n y: _limitValue(y, 0, chart.height - size.height)\n };\n}\n\nfunction getAlignedX(tooltip, align, options) {\n const padding = toPadding(options.padding);\n\n return align === 'center'\n ? tooltip.x + tooltip.width / 2\n : align === 'right'\n ? tooltip.x + tooltip.width - padding.right\n : tooltip.x + padding.left;\n}\n\n/**\n * Helper to build before and after body lines\n */\nfunction getBeforeAfterBodyLines(callback) {\n return pushOrConcat([], splitNewlines(callback));\n}\n\nfunction createTooltipContext(parent, tooltip, tooltipItems) {\n return createContext(parent, {\n tooltip,\n tooltipItems,\n type: 'tooltip'\n });\n}\n\nfunction overrideCallbacks(callbacks, context) {\n const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks;\n return override ? callbacks.override(override) : callbacks;\n}\n\nconst defaultCallbacks = {\n // Args are: (tooltipItems, data)\n beforeTitle: noop,\n title(tooltipItems) {\n if (tooltipItems.length > 0) {\n const item = tooltipItems[0];\n const labels = item.chart.data.labels;\n const labelCount = labels ? labels.length : 0;\n\n if (this && this.options && this.options.mode === 'dataset') {\n return item.dataset.label || '';\n } else if (item.label) {\n return item.label;\n } else if (labelCount > 0 && item.dataIndex < labelCount) {\n return labels[item.dataIndex];\n }\n }\n\n return '';\n },\n afterTitle: noop,\n\n // Args are: (tooltipItems, data)\n beforeBody: noop,\n\n // Args are: (tooltipItem, data)\n beforeLabel: noop,\n label(tooltipItem) {\n if (this && this.options && this.options.mode === 'dataset') {\n return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue;\n }\n\n let label = tooltipItem.dataset.label || '';\n\n if (label) {\n label += ': ';\n }\n const value = tooltipItem.formattedValue;\n if (!isNullOrUndef(value)) {\n label += value;\n }\n return label;\n },\n labelColor(tooltipItem) {\n const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);\n const options = meta.controller.getStyle(tooltipItem.dataIndex);\n return {\n borderColor: options.borderColor,\n backgroundColor: options.backgroundColor,\n borderWidth: options.borderWidth,\n borderDash: options.borderDash,\n borderDashOffset: options.borderDashOffset,\n borderRadius: 0,\n };\n },\n labelTextColor() {\n return this.options.bodyColor;\n },\n labelPointStyle(tooltipItem) {\n const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex);\n const options = meta.controller.getStyle(tooltipItem.dataIndex);\n return {\n pointStyle: options.pointStyle,\n rotation: options.rotation,\n };\n },\n afterLabel: noop,\n\n // Args are: (tooltipItems, data)\n afterBody: noop,\n\n // Args are: (tooltipItems, data)\n beforeFooter: noop,\n footer: noop,\n afterFooter: noop\n};\n\n/**\n * Invoke callback from object with context and arguments.\n * If callback returns `undefined`, then will be invoked default callback.\n * @param {Record} callbacks\n * @param {keyof typeof defaultCallbacks} name\n * @param {*} ctx\n * @param {*} arg\n * @returns {any}\n */\nfunction invokeCallbackWithFallback(callbacks, name, ctx, arg) {\n const result = callbacks[name].call(ctx, arg);\n\n if (typeof result === 'undefined') {\n return defaultCallbacks[name].call(ctx, arg);\n }\n\n return result;\n}\n\nexport class Tooltip extends Element {\n\n /**\n * @namespace Chart.Tooltip.positioners\n */\n static positioners = positioners;\n\n constructor(config) {\n super();\n\n this.opacity = 0;\n this._active = [];\n this._eventPosition = undefined;\n this._size = undefined;\n this._cachedAnimations = undefined;\n this._tooltipItems = [];\n this.$animations = undefined;\n this.$context = undefined;\n this.chart = config.chart;\n this.options = config.options;\n this.dataPoints = undefined;\n this.title = undefined;\n this.beforeBody = undefined;\n this.body = undefined;\n this.afterBody = undefined;\n this.footer = undefined;\n this.xAlign = undefined;\n this.yAlign = undefined;\n this.x = undefined;\n this.y = undefined;\n this.height = undefined;\n this.width = undefined;\n this.caretX = undefined;\n this.caretY = undefined;\n // TODO: V4, make this private, rename to `_labelStyles`, and combine with `labelPointStyles`\n // and `labelTextColors` to create a single variable\n this.labelColors = undefined;\n this.labelPointStyles = undefined;\n this.labelTextColors = undefined;\n }\n\n initialize(options) {\n this.options = options;\n this._cachedAnimations = undefined;\n this.$context = undefined;\n }\n\n /**\n\t * @private\n\t */\n _resolveAnimations() {\n const cached = this._cachedAnimations;\n\n if (cached) {\n return cached;\n }\n\n const chart = this.chart;\n const options = this.options.setContext(this.getContext());\n const opts = options.enabled && chart.options.animation && options.animations;\n const animations = new Animations(this.chart, opts);\n if (opts._cacheable) {\n this._cachedAnimations = Object.freeze(animations);\n }\n\n return animations;\n }\n\n /**\n\t * @protected\n\t */\n getContext() {\n return this.$context ||\n\t\t\t(this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems));\n }\n\n getTitle(context, options) {\n const {callbacks} = options;\n\n const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context);\n const title = invokeCallbackWithFallback(callbacks, 'title', this, context);\n const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context);\n\n let lines = [];\n lines = pushOrConcat(lines, splitNewlines(beforeTitle));\n lines = pushOrConcat(lines, splitNewlines(title));\n lines = pushOrConcat(lines, splitNewlines(afterTitle));\n\n return lines;\n }\n\n getBeforeBody(tooltipItems, options) {\n return getBeforeAfterBodyLines(\n invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems)\n );\n }\n\n getBody(tooltipItems, options) {\n const {callbacks} = options;\n const bodyItems = [];\n\n each(tooltipItems, (context) => {\n const bodyItem = {\n before: [],\n lines: [],\n after: []\n };\n const scoped = overrideCallbacks(callbacks, context);\n pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context)));\n pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context));\n pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context)));\n\n bodyItems.push(bodyItem);\n });\n\n return bodyItems;\n }\n\n getAfterBody(tooltipItems, options) {\n return getBeforeAfterBodyLines(\n invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems)\n );\n }\n\n // Get the footer and beforeFooter and afterFooter lines\n getFooter(tooltipItems, options) {\n const {callbacks} = options;\n\n const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems);\n const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems);\n const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems);\n\n let lines = [];\n lines = pushOrConcat(lines, splitNewlines(beforeFooter));\n lines = pushOrConcat(lines, splitNewlines(footer));\n lines = pushOrConcat(lines, splitNewlines(afterFooter));\n\n return lines;\n }\n\n /**\n\t * @private\n\t */\n _createItems(options) {\n const active = this._active;\n const data = this.chart.data;\n const labelColors = [];\n const labelPointStyles = [];\n const labelTextColors = [];\n let tooltipItems = [];\n let i, len;\n\n for (i = 0, len = active.length; i < len; ++i) {\n tooltipItems.push(createTooltipItem(this.chart, active[i]));\n }\n\n // If the user provided a filter function, use it to modify the tooltip items\n if (options.filter) {\n tooltipItems = tooltipItems.filter((element, index, array) => options.filter(element, index, array, data));\n }\n\n // If the user provided a sorting function, use it to modify the tooltip items\n if (options.itemSort) {\n tooltipItems = tooltipItems.sort((a, b) => options.itemSort(a, b, data));\n }\n\n // Determine colors for boxes\n each(tooltipItems, (context) => {\n const scoped = overrideCallbacks(options.callbacks, context);\n labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context));\n labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context));\n labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context));\n });\n\n this.labelColors = labelColors;\n this.labelPointStyles = labelPointStyles;\n this.labelTextColors = labelTextColors;\n this.dataPoints = tooltipItems;\n return tooltipItems;\n }\n\n update(changed, replay) {\n const options = this.options.setContext(this.getContext());\n const active = this._active;\n let properties;\n let tooltipItems = [];\n\n if (!active.length) {\n if (this.opacity !== 0) {\n properties = {\n opacity: 0\n };\n }\n } else {\n const position = positioners[options.position].call(this, active, this._eventPosition);\n tooltipItems = this._createItems(options);\n\n this.title = this.getTitle(tooltipItems, options);\n this.beforeBody = this.getBeforeBody(tooltipItems, options);\n this.body = this.getBody(tooltipItems, options);\n this.afterBody = this.getAfterBody(tooltipItems, options);\n this.footer = this.getFooter(tooltipItems, options);\n\n const size = this._size = getTooltipSize(this, options);\n const positionAndSize = Object.assign({}, position, size);\n const alignment = determineAlignment(this.chart, options, positionAndSize);\n const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart);\n\n this.xAlign = alignment.xAlign;\n this.yAlign = alignment.yAlign;\n\n properties = {\n opacity: 1,\n x: backgroundPoint.x,\n y: backgroundPoint.y,\n width: size.width,\n height: size.height,\n caretX: position.x,\n caretY: position.y\n };\n }\n\n this._tooltipItems = tooltipItems;\n this.$context = undefined;\n\n if (properties) {\n this._resolveAnimations().update(this, properties);\n }\n\n if (changed && options.external) {\n options.external.call(this, {chart: this.chart, tooltip: this, replay});\n }\n }\n\n drawCaret(tooltipPoint, ctx, size, options) {\n const caretPosition = this.getCaretPosition(tooltipPoint, size, options);\n\n ctx.lineTo(caretPosition.x1, caretPosition.y1);\n ctx.lineTo(caretPosition.x2, caretPosition.y2);\n ctx.lineTo(caretPosition.x3, caretPosition.y3);\n }\n\n getCaretPosition(tooltipPoint, size, options) {\n const {xAlign, yAlign} = this;\n const {caretSize, cornerRadius} = options;\n const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius);\n const {x: ptX, y: ptY} = tooltipPoint;\n const {width, height} = size;\n let x1, x2, x3, y1, y2, y3;\n\n if (yAlign === 'center') {\n y2 = ptY + (height / 2);\n\n if (xAlign === 'left') {\n x1 = ptX;\n x2 = x1 - caretSize;\n\n // Left draws bottom -> top, this y1 is on the bottom\n y1 = y2 + caretSize;\n y3 = y2 - caretSize;\n } else {\n x1 = ptX + width;\n x2 = x1 + caretSize;\n\n // Right draws top -> bottom, thus y1 is on the top\n y1 = y2 - caretSize;\n y3 = y2 + caretSize;\n }\n\n x3 = x1;\n } else {\n if (xAlign === 'left') {\n x2 = ptX + Math.max(topLeft, bottomLeft) + (caretSize);\n } else if (xAlign === 'right') {\n x2 = ptX + width - Math.max(topRight, bottomRight) - caretSize;\n } else {\n x2 = this.caretX;\n }\n\n if (yAlign === 'top') {\n y1 = ptY;\n y2 = y1 - caretSize;\n\n // Top draws left -> right, thus x1 is on the left\n x1 = x2 - caretSize;\n x3 = x2 + caretSize;\n } else {\n y1 = ptY + height;\n y2 = y1 + caretSize;\n\n // Bottom draws right -> left, thus x1 is on the right\n x1 = x2 + caretSize;\n x3 = x2 - caretSize;\n }\n y3 = y1;\n }\n return {x1, x2, x3, y1, y2, y3};\n }\n\n drawTitle(pt, ctx, options) {\n const title = this.title;\n const length = title.length;\n let titleFont, titleSpacing, i;\n\n if (length) {\n const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);\n\n pt.x = getAlignedX(this, options.titleAlign, options);\n\n ctx.textAlign = rtlHelper.textAlign(options.titleAlign);\n ctx.textBaseline = 'middle';\n\n titleFont = toFont(options.titleFont);\n titleSpacing = options.titleSpacing;\n\n ctx.fillStyle = options.titleColor;\n ctx.font = titleFont.string;\n\n for (i = 0; i < length; ++i) {\n ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2);\n pt.y += titleFont.lineHeight + titleSpacing; // Line Height and spacing\n\n if (i + 1 === length) {\n pt.y += options.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing\n }\n }\n }\n }\n\n /**\n\t * @private\n\t */\n _drawColorBox(ctx, pt, i, rtlHelper, options) {\n const labelColor = this.labelColors[i];\n const labelPointStyle = this.labelPointStyles[i];\n const {boxHeight, boxWidth} = options;\n const bodyFont = toFont(options.bodyFont);\n const colorX = getAlignedX(this, 'left', options);\n const rtlColorX = rtlHelper.x(colorX);\n const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0;\n const colorY = pt.y + yOffSet;\n\n if (options.usePointStyle) {\n const drawOptions = {\n radius: Math.min(boxWidth, boxHeight) / 2, // fit the circle in the box\n pointStyle: labelPointStyle.pointStyle,\n rotation: labelPointStyle.rotation,\n borderWidth: 1\n };\n // Recalculate x and y for drawPoint() because its expecting\n // x and y to be center of figure (instead of top left)\n const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2;\n const centerY = colorY + boxHeight / 2;\n\n // Fill the point with white so that colours merge nicely if the opacity is < 1\n ctx.strokeStyle = options.multiKeyBackground;\n ctx.fillStyle = options.multiKeyBackground;\n drawPoint(ctx, drawOptions, centerX, centerY);\n\n // Draw the point\n ctx.strokeStyle = labelColor.borderColor;\n ctx.fillStyle = labelColor.backgroundColor;\n drawPoint(ctx, drawOptions, centerX, centerY);\n } else {\n // Border\n ctx.lineWidth = isObject(labelColor.borderWidth) ? Math.max(...Object.values(labelColor.borderWidth)) : (labelColor.borderWidth || 1); // TODO, v4 remove fallback\n ctx.strokeStyle = labelColor.borderColor;\n ctx.setLineDash(labelColor.borderDash || []);\n ctx.lineDashOffset = labelColor.borderDashOffset || 0;\n\n // Fill a white rect so that colours merge nicely if the opacity is < 1\n const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth);\n const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2);\n const borderRadius = toTRBLCorners(labelColor.borderRadius);\n\n if (Object.values(borderRadius).some(v => v !== 0)) {\n ctx.beginPath();\n ctx.fillStyle = options.multiKeyBackground;\n addRoundedRectPath(ctx, {\n x: outerX,\n y: colorY,\n w: boxWidth,\n h: boxHeight,\n radius: borderRadius,\n });\n ctx.fill();\n ctx.stroke();\n\n // Inner square\n ctx.fillStyle = labelColor.backgroundColor;\n ctx.beginPath();\n addRoundedRectPath(ctx, {\n x: innerX,\n y: colorY + 1,\n w: boxWidth - 2,\n h: boxHeight - 2,\n radius: borderRadius,\n });\n ctx.fill();\n } else {\n // Normal rect\n ctx.fillStyle = options.multiKeyBackground;\n ctx.fillRect(outerX, colorY, boxWidth, boxHeight);\n ctx.strokeRect(outerX, colorY, boxWidth, boxHeight);\n // Inner square\n ctx.fillStyle = labelColor.backgroundColor;\n ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2);\n }\n }\n\n // restore fillStyle\n ctx.fillStyle = this.labelTextColors[i];\n }\n\n drawBody(pt, ctx, options) {\n const {body} = this;\n const {bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth, boxPadding} = options;\n const bodyFont = toFont(options.bodyFont);\n let bodyLineHeight = bodyFont.lineHeight;\n let xLinePadding = 0;\n\n const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);\n\n const fillLineOfText = function(line) {\n ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2);\n pt.y += bodyLineHeight + bodySpacing;\n };\n\n const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign);\n let bodyItem, textColor, lines, i, j, ilen, jlen;\n\n ctx.textAlign = bodyAlign;\n ctx.textBaseline = 'middle';\n ctx.font = bodyFont.string;\n\n pt.x = getAlignedX(this, bodyAlignForCalculation, options);\n\n // Before body lines\n ctx.fillStyle = options.bodyColor;\n each(this.beforeBody, fillLineOfText);\n\n xLinePadding = displayColors && bodyAlignForCalculation !== 'right'\n ? bodyAlign === 'center' ? (boxWidth / 2 + boxPadding) : (boxWidth + 2 + boxPadding)\n : 0;\n\n // Draw body lines now\n for (i = 0, ilen = body.length; i < ilen; ++i) {\n bodyItem = body[i];\n textColor = this.labelTextColors[i];\n\n ctx.fillStyle = textColor;\n each(bodyItem.before, fillLineOfText);\n\n lines = bodyItem.lines;\n // Draw Legend-like boxes if needed\n if (displayColors && lines.length) {\n this._drawColorBox(ctx, pt, i, rtlHelper, options);\n bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight);\n }\n\n for (j = 0, jlen = lines.length; j < jlen; ++j) {\n fillLineOfText(lines[j]);\n // Reset for any lines that don't include colorbox\n bodyLineHeight = bodyFont.lineHeight;\n }\n\n each(bodyItem.after, fillLineOfText);\n }\n\n // Reset back to 0 for after body\n xLinePadding = 0;\n bodyLineHeight = bodyFont.lineHeight;\n\n // After body lines\n each(this.afterBody, fillLineOfText);\n pt.y -= bodySpacing; // Remove last body spacing\n }\n\n drawFooter(pt, ctx, options) {\n const footer = this.footer;\n const length = footer.length;\n let footerFont, i;\n\n if (length) {\n const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width);\n\n pt.x = getAlignedX(this, options.footerAlign, options);\n pt.y += options.footerMarginTop;\n\n ctx.textAlign = rtlHelper.textAlign(options.footerAlign);\n ctx.textBaseline = 'middle';\n\n footerFont = toFont(options.footerFont);\n\n ctx.fillStyle = options.footerColor;\n ctx.font = footerFont.string;\n\n for (i = 0; i < length; ++i) {\n ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2);\n pt.y += footerFont.lineHeight + options.footerSpacing;\n }\n }\n }\n\n drawBackground(pt, ctx, tooltipSize, options) {\n const {xAlign, yAlign} = this;\n const {x, y} = pt;\n const {width, height} = tooltipSize;\n const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(options.cornerRadius);\n\n ctx.fillStyle = options.backgroundColor;\n ctx.strokeStyle = options.borderColor;\n ctx.lineWidth = options.borderWidth;\n\n ctx.beginPath();\n ctx.moveTo(x + topLeft, y);\n if (yAlign === 'top') {\n this.drawCaret(pt, ctx, tooltipSize, options);\n }\n ctx.lineTo(x + width - topRight, y);\n ctx.quadraticCurveTo(x + width, y, x + width, y + topRight);\n if (yAlign === 'center' && xAlign === 'right') {\n this.drawCaret(pt, ctx, tooltipSize, options);\n }\n ctx.lineTo(x + width, y + height - bottomRight);\n ctx.quadraticCurveTo(x + width, y + height, x + width - bottomRight, y + height);\n if (yAlign === 'bottom') {\n this.drawCaret(pt, ctx, tooltipSize, options);\n }\n ctx.lineTo(x + bottomLeft, y + height);\n ctx.quadraticCurveTo(x, y + height, x, y + height - bottomLeft);\n if (yAlign === 'center' && xAlign === 'left') {\n this.drawCaret(pt, ctx, tooltipSize, options);\n }\n ctx.lineTo(x, y + topLeft);\n ctx.quadraticCurveTo(x, y, x + topLeft, y);\n ctx.closePath();\n\n ctx.fill();\n\n if (options.borderWidth > 0) {\n ctx.stroke();\n }\n }\n\n /**\n\t * Update x/y animation targets when _active elements are animating too\n\t * @private\n\t */\n _updateAnimationTarget(options) {\n const chart = this.chart;\n const anims = this.$animations;\n const animX = anims && anims.x;\n const animY = anims && anims.y;\n if (animX || animY) {\n const position = positioners[options.position].call(this, this._active, this._eventPosition);\n if (!position) {\n return;\n }\n const size = this._size = getTooltipSize(this, options);\n const positionAndSize = Object.assign({}, position, this._size);\n const alignment = determineAlignment(chart, options, positionAndSize);\n const point = getBackgroundPoint(options, positionAndSize, alignment, chart);\n if (animX._to !== point.x || animY._to !== point.y) {\n this.xAlign = alignment.xAlign;\n this.yAlign = alignment.yAlign;\n this.width = size.width;\n this.height = size.height;\n this.caretX = position.x;\n this.caretY = position.y;\n this._resolveAnimations().update(this, point);\n }\n }\n }\n\n /**\n * Determine if the tooltip will draw anything\n * @returns {boolean} True if the tooltip will render\n */\n _willRender() {\n return !!this.opacity;\n }\n\n draw(ctx) {\n const options = this.options.setContext(this.getContext());\n let opacity = this.opacity;\n\n if (!opacity) {\n return;\n }\n\n this._updateAnimationTarget(options);\n\n const tooltipSize = {\n width: this.width,\n height: this.height\n };\n const pt = {\n x: this.x,\n y: this.y\n };\n\n // IE11/Edge does not like very small opacities, so snap to 0\n opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity;\n\n const padding = toPadding(options.padding);\n\n // Truthy/falsey value for empty tooltip\n const hasTooltipContent = this.title.length || this.beforeBody.length || this.body.length || this.afterBody.length || this.footer.length;\n\n if (options.enabled && hasTooltipContent) {\n ctx.save();\n ctx.globalAlpha = opacity;\n\n // Draw Background\n this.drawBackground(pt, ctx, tooltipSize, options);\n\n overrideTextDirection(ctx, options.textDirection);\n\n pt.y += padding.top;\n\n // Titles\n this.drawTitle(pt, ctx, options);\n\n // Body\n this.drawBody(pt, ctx, options);\n\n // Footer\n this.drawFooter(pt, ctx, options);\n\n restoreTextDirection(ctx, options.textDirection);\n\n ctx.restore();\n }\n }\n\n /**\n\t * Get active elements in the tooltip\n\t * @returns {Array} Array of elements that are active in the tooltip\n\t */\n getActiveElements() {\n return this._active || [];\n }\n\n /**\n\t * Set active elements in the tooltip\n\t * @param {array} activeElements Array of active datasetIndex/index pairs.\n\t * @param {object} eventPosition Synthetic event position used in positioning\n\t */\n setActiveElements(activeElements, eventPosition) {\n const lastActive = this._active;\n const active = activeElements.map(({datasetIndex, index}) => {\n const meta = this.chart.getDatasetMeta(datasetIndex);\n\n if (!meta) {\n throw new Error('Cannot find a dataset at index ' + datasetIndex);\n }\n\n return {\n datasetIndex,\n element: meta.data[index],\n index,\n };\n });\n const changed = !_elementsEqual(lastActive, active);\n const positionChanged = this._positionChanged(active, eventPosition);\n\n if (changed || positionChanged) {\n this._active = active;\n this._eventPosition = eventPosition;\n this._ignoreReplayEvents = true;\n this.update(true);\n }\n }\n\n /**\n\t * Handle an event\n\t * @param {ChartEvent} e - The event to handle\n\t * @param {boolean} [replay] - This is a replayed event (from update)\n\t * @param {boolean} [inChartArea] - The event is inside chartArea\n\t * @returns {boolean} true if the tooltip changed\n\t */\n handleEvent(e, replay, inChartArea = true) {\n if (replay && this._ignoreReplayEvents) {\n return false;\n }\n this._ignoreReplayEvents = false;\n\n const options = this.options;\n const lastActive = this._active || [];\n const active = this._getActiveElements(e, lastActive, replay, inChartArea);\n\n // When there are multiple items shown, but the tooltip position is nearest mode\n // an update may need to be made because our position may have changed even though\n // the items are the same as before.\n const positionChanged = this._positionChanged(active, e);\n\n // Remember Last Actives\n const changed = replay || !_elementsEqual(active, lastActive) || positionChanged;\n\n // Only handle target event on tooltip change\n if (changed) {\n this._active = active;\n\n if (options.enabled || options.external) {\n this._eventPosition = {\n x: e.x,\n y: e.y\n };\n\n this.update(true, replay);\n }\n }\n\n return changed;\n }\n\n /**\n\t * Helper for determining the active elements for event\n\t * @param {ChartEvent} e - The event to handle\n\t * @param {InteractionItem[]} lastActive - Previously active elements\n\t * @param {boolean} [replay] - This is a replayed event (from update)\n\t * @param {boolean} [inChartArea] - The event is inside chartArea\n\t * @returns {InteractionItem[]} - Active elements\n\t * @private\n\t */\n _getActiveElements(e, lastActive, replay, inChartArea) {\n const options = this.options;\n\n if (e.type === 'mouseout') {\n return [];\n }\n\n if (!inChartArea) {\n // Let user control the active elements outside chartArea. Eg. using Legend.\n // But make sure that active elements are still valid.\n return lastActive.filter(i =>\n this.chart.data.datasets[i.datasetIndex] &&\n this.chart.getDatasetMeta(i.datasetIndex).controller.getParsed(i.index) !== undefined\n );\n }\n\n // Find Active Elements for tooltips\n const active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay);\n\n if (options.reverse) {\n active.reverse();\n }\n\n return active;\n }\n\n /**\n\t * Determine if the active elements + event combination changes the\n\t * tooltip position\n\t * @param {array} active - Active elements\n\t * @param {ChartEvent} e - Event that triggered the position change\n\t * @returns {boolean} True if the position has changed\n\t */\n _positionChanged(active, e) {\n const {caretX, caretY, options} = this;\n const position = positioners[options.position].call(this, active, e);\n return position !== false && (caretX !== position.x || caretY !== position.y);\n }\n}\n\nexport default {\n id: 'tooltip',\n _element: Tooltip,\n positioners,\n\n afterInit(chart, _args, options) {\n if (options) {\n chart.tooltip = new Tooltip({chart, options});\n }\n },\n\n beforeUpdate(chart, _args, options) {\n if (chart.tooltip) {\n chart.tooltip.initialize(options);\n }\n },\n\n reset(chart, _args, options) {\n if (chart.tooltip) {\n chart.tooltip.initialize(options);\n }\n },\n\n afterDraw(chart) {\n const tooltip = chart.tooltip;\n\n if (tooltip && tooltip._willRender()) {\n const args = {\n tooltip\n };\n\n if (chart.notifyPlugins('beforeTooltipDraw', {...args, cancelable: true}) === false) {\n return;\n }\n\n tooltip.draw(chart.ctx);\n\n chart.notifyPlugins('afterTooltipDraw', args);\n }\n },\n\n afterEvent(chart, args) {\n if (chart.tooltip) {\n // If the event is replayed from `update`, we should evaluate with the final positions.\n const useFinalPosition = args.replay;\n if (chart.tooltip.handleEvent(args.event, useFinalPosition, args.inChartArea)) {\n // notify chart about the change, so it will render\n args.changed = true;\n }\n }\n },\n\n defaults: {\n enabled: true,\n external: null,\n position: 'average',\n backgroundColor: 'rgba(0,0,0,0.8)',\n titleColor: '#fff',\n titleFont: {\n weight: 'bold',\n },\n titleSpacing: 2,\n titleMarginBottom: 6,\n titleAlign: 'left',\n bodyColor: '#fff',\n bodySpacing: 2,\n bodyFont: {\n },\n bodyAlign: 'left',\n footerColor: '#fff',\n footerSpacing: 2,\n footerMarginTop: 6,\n footerFont: {\n weight: 'bold',\n },\n footerAlign: 'left',\n padding: 6,\n caretPadding: 2,\n caretSize: 5,\n cornerRadius: 6,\n boxHeight: (ctx, opts) => opts.bodyFont.size,\n boxWidth: (ctx, opts) => opts.bodyFont.size,\n multiKeyBackground: '#fff',\n displayColors: true,\n boxPadding: 0,\n borderColor: 'rgba(0,0,0,0)',\n borderWidth: 0,\n animation: {\n duration: 400,\n easing: 'easeOutQuart',\n },\n animations: {\n numbers: {\n type: 'number',\n properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'],\n },\n opacity: {\n easing: 'linear',\n duration: 200\n }\n },\n callbacks: defaultCallbacks\n },\n\n defaultRoutes: {\n bodyFont: 'font',\n footerFont: 'font',\n titleFont: 'font'\n },\n\n descriptors: {\n _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'external',\n _indexable: false,\n callbacks: {\n _scriptable: false,\n _indexable: false,\n },\n animation: {\n _fallback: false\n },\n animations: {\n _fallback: 'animation'\n }\n },\n\n // Resolve additionally from `interaction` options and defaults.\n additionalOptionScopes: ['interaction']\n};\n","// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\n\n/**\n * @namespace Chart\n */\nimport Chart from './core/core.controller.js';\n\nimport * as helpers from './helpers/index.js';\nimport _adapters from './core/core.adapters.js';\nimport Animation from './core/core.animation.js';\nimport animator from './core/core.animator.js';\nimport Animations from './core/core.animations.js';\nimport * as controllers from './controllers/index.js';\nimport DatasetController from './core/core.datasetController.js';\nimport Element from './core/core.element.js';\nimport * as elements from './elements/index.js';\nimport Interaction from './core/core.interaction.js';\nimport layouts from './core/core.layouts.js';\nimport * as platforms from './platform/index.js';\nimport * as plugins from './plugins/index.js';\nimport registry from './core/core.registry.js';\nimport Scale from './core/core.scale.js';\nimport * as scales from './scales/index.js';\nimport Ticks from './core/core.ticks.js';\n\n// Register built-ins\nChart.register(controllers, scales, elements, plugins);\n\nChart.helpers = {...helpers};\nChart._adapters = _adapters;\nChart.Animation = Animation;\nChart.Animations = Animations;\nChart.animator = animator;\nChart.controllers = registry.controllers.items;\nChart.DatasetController = DatasetController;\nChart.Element = Element;\nChart.elements = elements;\nChart.Interaction = Interaction;\nChart.layouts = layouts;\nChart.platforms = platforms;\nChart.Scale = Scale;\nChart.Ticks = Ticks;\n\n// Compatibility with ESM extensions\nObject.assign(Chart, controllers, scales, elements, plugins, platforms);\nChart.Chart = Chart;\n\nif (typeof window !== 'undefined') {\n window.Chart = Chart;\n}\n\nexport default Chart;\n\n"],"names":["noop","uid","id","isNullOrUndef","value","isArray","Array","type","Object","prototype","toString","call","slice","isObject","isNumberFinite","Number","isFinite","finiteOrDefault","defaultValue","valueOrDefault","toPercentage","dimension","endsWith","parseFloat","toDimension","callback","fn","args","thisArg","apply","each","loopable","reverse","i","len","keys","length","_elementsEqual","a0","a1","ilen","v0","v1","datasetIndex","index","clone","source","map","target","create","klen","k","isValidKey","key","indexOf","_merger","options","tval","sval","merge","sources","merger","current","mergeIf","_mergerIf","hasOwnProperty","keyResolvers","v","x","o","y","_splitKey","parts","split","tmp","part","push","resolveObjectKey","obj","resolver","_getKeyResolver","_capitalize","str","charAt","toUpperCase","defined","isFunction","setsEqual","a","b","size","item","has","_isClickEvent","e","PI","Math","TAU","PITAU","INFINITY","POSITIVE_INFINITY","RAD_PER_DEG","HALF_PI","QUARTER_PI","TWO_THIRDS_PI","log10","sign","almostEquals","epsilon","abs","niceNum","range","roundedRange","round","niceRange","pow","floor","fraction","_factorize","result","sqrt","sort","pop","isNumber","n","isNaN","almostWhole","rounded","_setMinAndMaxByKey","array","property","min","max","toRadians","degrees","toDegrees","radians","_decimalPlaces","isFiniteNumber","p","getAngleFromPoint","centrePoint","anglePoint","distanceFromXCenter","distanceFromYCenter","radialDistanceFromCenter","angle","atan2","distance","distanceBetweenPoints","pt1","pt2","_angleDiff","_normalizeAngle","_angleBetween","start","end","sameAngleIsFullCircle","s","angleToStart","angleToEnd","startToAngle","endToAngle","_limitValue","_int16Range","_isBetween","_lookup","table","cmp","mid","hi","lo","_lookupByKey","last","ti","_rlookupByKey","_filterBetween","values","arrayEvents","listenArrayEvents","listener","_chartjs","listeners","defineProperty","configurable","enumerable","forEach","method","base","res","this","object","unlistenArrayEvents","stub","splice","_arrayUnique","items","set","Set","from","requestAnimFrame","window","requestAnimationFrame","throttled","argsToUse","ticking","debounce","delay","timeout","clearTimeout","setTimeout","_toLeftRightCenter","align","_alignStartEnd","_textX","left","right","rtl","_getStartAndCountOfVisiblePoints","meta","points","animationsDisabled","pointCount","count","_sorted","iScale","_parsed","axis","minDefined","maxDefined","getUserBounds","getPixelForValue","_scaleRangesChanged","xScale","yScale","_scaleRanges","newRanges","xmin","xmax","ymin","ymax","changed","assign","Animator","constructor","_request","_charts","Map","_running","_lastDate","undefined","_notify","chart","anims","date","callbacks","numSteps","duration","initial","currentStep","_refresh","_update","Date","now","remaining","running","draw","_active","_total","tick","_getAnims","charts","get","complete","progress","listen","event","cb","add","reduce","acc","cur","_duration","stop","cancel","remove","delete","animator","lim","l","h","p2b","n2b","b2n","n2p","map$1","A","B","C","D","E","F","c","d","f","hex","h1","h2","eq","hexString","r","g","isShort","alpha","HUE_RE","hsl2rgbn","hsv2rgbn","hwb2rgbn","w","rgb","rgb2hsl","hueValue","calln","hsl2rgb","hue","hueParse","m","exec","p1","p2","hwb2rgb","hsv2rgb","Z","Y","X","W","V","U","T","S","R","Q","P","O","N","M","L","K","G","H","I","J","names$1","OiceXe","antiquewEte","aqua","aquamarRe","azuY","beige","bisque","black","blanKedOmond","Xe","XeviTet","bPwn","burlywood","caMtXe","KartYuse","KocTate","cSO","cSnflowerXe","cSnsilk","crimson","cyan","xXe","xcyan","xgTMnPd","xWay","xgYF","xgYy","xkhaki","xmagFta","xTivegYF","xSange","xScEd","xYd","xsOmon","xsHgYF","xUXe","xUWay","xUgYy","xQe","xviTet","dAppRk","dApskyXe","dimWay","dimgYy","dodgerXe","fiYbrick","flSOwEte","foYstWAn","fuKsia","gaRsbSo","ghostwEte","gTd","gTMnPd","Way","gYF","gYFLw","gYy","honeyMw","hotpRk","RdianYd","Rdigo","ivSy","khaki","lavFMr","lavFMrXsh","lawngYF","NmoncEffon","ZXe","ZcSO","Zcyan","ZgTMnPdLw","ZWay","ZgYF","ZgYy","ZpRk","ZsOmon","ZsHgYF","ZskyXe","ZUWay","ZUgYy","ZstAlXe","ZLw","lime","limegYF","lRF","magFta","maPon","VaquamarRe","VXe","VScEd","VpurpN","VsHgYF","VUXe","VsprRggYF","VQe","VviTetYd","midnightXe","mRtcYam","mistyPse","moccasR","navajowEte","navy","Tdlace","Tive","TivedBb","Sange","SangeYd","ScEd","pOegTMnPd","pOegYF","pOeQe","pOeviTetYd","papayawEp","pHKpuff","peru","pRk","plum","powMrXe","purpN","YbeccapurpN","Yd","Psybrown","PyOXe","saddNbPwn","sOmon","sandybPwn","sHgYF","sHshell","siFna","silver","skyXe","UXe","UWay","UgYy","snow","sprRggYF","stAlXe","tan","teO","tEstN","tomato","Qe","viTet","JHt","wEte","wEtesmoke","Lw","LwgYF","names","nameParse","unpacked","tkeys","j","ok","nk","replace","parseInt","unpack","transparent","toLowerCase","RGB_RE","to","modHSL","ratio","proto","fromObject","input","functionParse","rgbParse","Color","ret","_rgb","_valid","valid","rgbString","hslString","mix","color","weight","c1","c2","w2","w1","interpolate","t","rgb1","rgb2","clearer","greyscale","val","opaquer","negate","lighten","darken","saturate","desaturate","rotate","deg","isPatternOrGradient","getHoverColor","numbers","colors","intlCache","formatNumber","num","locale","cacheKey","JSON","stringify","formatter","Intl","NumberFormat","getNumberFormat","format","formatters","numeric","tickValue","ticks","notation","delta","maxTick","calculateDelta","logDelta","numDecimal","minimumFractionDigits","maximumFractionDigits","logarithmic","remain","significand","includes","Ticks","overrides","descriptors","getScope","node","root","scope","Defaults","_descriptors","_appliers","animation","backgroundColor","borderColor","datasets","devicePixelRatio","context","platform","getDevicePixelRatio","elements","events","font","family","style","lineHeight","hover","hoverBackgroundColor","ctx","hoverBorderColor","hoverColor","indexAxis","interaction","mode","intersect","includeInvisible","maintainAspectRatio","onHover","onClick","parsing","plugins","responsive","scale","scales","showLine","drawActiveElementsOnTop","describe","override","route","name","targetScope","targetName","scopeObject","targetScopeObject","privateName","defineProperties","writable","local","appliers","defaults","_scriptable","startsWith","_indexable","_fallback","easing","loop","properties","active","resize","show","animations","visible","hide","autoPadding","padding","top","bottom","display","offset","beginAtZero","bounds","clip","grace","grid","lineWidth","drawOnChartArea","drawTicks","tickLength","tickWidth","_ctx","tickColor","border","dash","dashOffset","width","title","text","minRotation","maxRotation","mirror","textStrokeWidth","textStrokeColor","autoSkip","autoSkipPadding","labelOffset","minor","major","crossAlign","showLabelBackdrop","backdropColor","backdropPadding","_isDomSupported","document","_getParentNode","domNode","parent","parentNode","host","parseMaxStyle","styleValue","parentProperty","valueInPixels","getComputedStyle","element","ownerDocument","defaultView","getStyle","el","getPropertyValue","positions","getPositionedStyle","styles","suffix","pos","height","useOffsetPos","shadowRoot","getRelativePosition","canvas","currentDevicePixelRatio","borderBox","boxSizing","paddings","borders","box","touches","offsetX","offsetY","rect","getBoundingClientRect","clientX","clientY","getCanvasPosition","xOffset","yOffset","round1","getMaximumSize","bbWidth","bbHeight","aspectRatio","margins","maxWidth","maxHeight","containerSize","container","containerStyle","containerBorder","containerPadding","clientWidth","clientHeight","getContainerSize","retinaScale","forceRatio","forceStyle","pixelRatio","deviceHeight","deviceWidth","setTransform","supportsEventListenerOptions","passiveSupported","passive","addEventListener","removeEventListener","readUsedSize","matches","match","toFontString","_measureText","data","gc","longest","string","textWidth","measureText","_longestText","arrayOfThings","cache","garbageCollect","save","jlen","thing","nestedThing","restore","gcLen","_alignPixel","pixel","halfWidth","clearCanvas","getContext","resetTransform","clearRect","drawPoint","drawPointLegend","cornerRadius","xOffsetW","yOffsetW","pointStyle","rotation","radius","rad","translate","drawImage","beginPath","ellipse","arc","closePath","moveTo","sin","cos","lineTo","SQRT1_2","fill","borderWidth","stroke","_isPointInArea","point","area","margin","clipArea","unclipArea","_steppedLineTo","previous","flip","midpoint","_bezierCurveTo","bezierCurveTo","cp1x","cp2x","cp1y","cp2y","decorateText","line","opts","strikethrough","underline","metrics","actualBoundingBoxLeft","actualBoundingBoxRight","actualBoundingBoxAscent","actualBoundingBoxDescent","yDecoration","strokeStyle","fillStyle","decorationWidth","drawBackdrop","oldColor","fillRect","renderText","lines","strokeWidth","strokeColor","translation","textAlign","textBaseline","setRenderOpts","backdrop","strokeText","fillText","addRoundedRectPath","topLeft","bottomLeft","bottomRight","topRight","_createResolver","scopes","prefixes","rootScopes","fallback","getTarget","finalRootScopes","_resolve","Symbol","toStringTag","_cacheable","_scopes","_rootScopes","_getTarget","Proxy","deleteProperty","prop","_keys","_cached","proxy","prefix","readKey","needsSubResolver","createSubResolver","_resolveWithPrefixes","getOwnPropertyDescriptor","Reflect","getPrototypeOf","getKeysFromAllScopes","ownKeys","storage","_storage","_attachContext","subProxy","descriptorDefaults","_proxy","_context","_subProxy","_stack","setContext","receiver","isScriptable","getValue","Error","join","_resolveScriptable","isIndexable","arr","filter","_resolveArray","_resolveWithContext","allKeys","scriptable","indexable","_allKeys","resolve","resolveFallback","addScopes","parentScopes","parentFallback","allScopes","addScopesFromKey","subGetTarget","resolveKeysFromAllScopes","_parseObjectDataRadialScale","_parsing","parsed","parse","EPSILON","getPoint","skip","getValueAxis","splineCurve","firstPoint","middlePoint","afterPoint","next","d01","d12","s01","s12","fa","fb","splineCurveMonotone","valueAxis","pointsLen","deltaK","mK","pointBefore","pointCurrent","pointAfter","slopeDelta","alphaK","betaK","tauK","squaredMagnitude","monotoneAdjust","iPixel","vPixel","monotoneCompute","capControlPoint","pt","_updateBezierControlPoints","controlPoints","spanGaps","cubicInterpolationMode","prev","tension","capBezierPoints","inArea","inAreaPrev","inAreaNext","atEdge","elasticIn","elasticOut","effects","linear","easeInQuad","easeOutQuad","easeInOutQuad","easeInCubic","easeOutCubic","easeInOutCubic","easeInQuart","easeOutQuart","easeInOutQuart","easeInQuint","easeOutQuint","easeInOutQuint","easeInSine","easeOutSine","easeInOutSine","easeInExpo","easeOutExpo","easeInOutExpo","easeInCirc","easeOutCirc","easeInOutCirc","easeInElastic","easeOutElastic","easeInOutElastic","easeInBack","easeOutBack","easeInOutBack","easeInBounce","easeOutBounce","easeInOutBounce","_pointInLine","_steppedInterpolation","_bezierInterpolation","cp1","cp2","LINE_HEIGHT","FONT_STYLE","toLineHeight","numberOrZero","_readValueToProps","props","objProps","read","toTRBL","toTRBLCorners","toPadding","toFont","console","warn","inputs","info","cacheable","_addGrace","minmax","change","keepZero","createContext","parentContext","getRtlAdapter","rectX","setWidth","xPlus","leftForLtr","itemWidth","getRightToLeftAdapter","_itemWidth","overrideTextDirection","direction","original","getPropertyPriority","setProperty","prevTextDirection","restoreTextDirection","propertyFn","between","compare","normalize","normalizeSegment","_boundSegment","segment","startBound","endBound","getSegment","prevValue","inside","subStart","shouldStart","shouldStop","_boundSegments","segments","sub","_computeSegments","segmentOptions","_loop","findStartAndEnd","splitByStyles","solidSegments","_fullLoop","chartContext","_chart","baseStyle","readStyle","_datasetIndex","prevStyle","addStyle","st","dir","p0","p0DataIndex","p1DataIndex","styleChanged","doSplitByStyles","borderCapStyle","borderDash","borderDashOffset","borderJoinStyle","replacer","pixelSize","fontStyle","fontFamily","binarySearch","metaset","controller","_cachedMeta","lookupMethod","_reversePixels","_sharedOptions","getRange","evaluateInteractionItems","position","handler","metasets","getSortedVisibleDatasetMetas","getIntersectItems","useFinalPosition","isPointInArea","chartArea","inRange","getNearestCartesianItems","distanceMetric","useX","useY","deltaX","deltaY","getDistanceMetricForAxis","minDistance","center","getCenterPoint","getNearestItems","startAngle","endAngle","getProps","getNearestRadialItems","getAxisItems","rangeMethod","intersectsItem","Interaction","modes","dataset","getDatasetMeta","nearest","STATIC_POSITIONS","filterByPosition","filterDynamicPositionByAxis","sortByWeight","setLayoutDims","layouts","params","stacks","wrap","stack","stackWeight","placed","buildStacks","vBoxMaxWidth","hBoxMaxHeight","layout","fullSize","factor","horizontal","availableWidth","availableHeight","getCombinedMax","maxPadding","updateMaxPadding","boxPadding","updateDims","getPadding","newWidth","outerWidth","newHeight","outerHeight","widthChanged","heightChanged","same","other","getMargins","marginForPositions","fitBoxes","boxes","refitBoxes","refit","update","setBoxDims","placeBoxes","userPadding","addBox","_layers","z","removeBox","layoutItem","configure","minPadding","layoutBoxes","isHorizontal","wrapBoxes","centerHorizontal","centerVertical","leftAndTop","concat","rightAndBottom","vertical","buildLayoutBoxes","verticalBoxes","horizontalBoxes","beforeLayout","visibleVerticalBoxCount","total","freeze","updatePos","handleMaxPadding","BasePlatform","acquireContext","releaseContext","isAttached","updateConfig","config","BasicPlatform","EXPANDO_KEY","EVENT_TYPES","touchstart","touchmove","touchend","pointerenter","pointerdown","pointermove","pointerup","pointerleave","pointerout","isNullOrEmpty","eventListenerOptions","removeListener","nodeListContains","nodeList","contains","createAttachObserver","observer","MutationObserver","entries","trigger","entry","addedNodes","removedNodes","observe","childList","subtree","createDetachObserver","drpListeningCharts","oldDevicePixelRatio","onWindowResize","dpr","createResizeObserver","ResizeObserver","contentRect","listenDevicePixelRatioChanges","releaseObserver","disconnect","unlistenDevicePixelRatioChanges","createProxyAndListen","native","fromNativeEvent","addListener","DomPlatform","renderHeight","getAttribute","renderWidth","displayWidth","displayHeight","initCanvas","removeAttribute","setAttribute","proxies","$proxies","attach","detach","isConnected","_detectPlatform","OffscreenCanvas","interpolators","boolean","c0","helpersColor","number","Animation","cfg","currentValue","_fn","_easing","_start","_target","_prop","_from","_to","_promises","elapsed","wait","promises","Promise","rej","resolved","Animations","_properties","animationOptions","animatedProps","getOwnPropertyNames","option","_animateOptions","newOptions","$shared","$animations","resolveTargetOptions","_createAnimations","anim","all","awaitAll","then","scaleClip","allowedOverflow","getSortedDatasetIndices","filterVisible","_getSortedDatasetMetas","applyStack","dsIndex","singleMode","otherValue","found","isStacked","stacked","getOrCreateStack","stackKey","indexValue","subStack","getLastIndexInStack","vScale","positive","getMatchingVisibleMetas","updateStacks","_stacks","iAxis","vAxis","indexScale","valueScale","getStackKey","_top","_bottom","_visualValues","getFirstScaleId","shift","clearStacks","isDirectUpdateMode","cloneIfNotShared","cached","shared","DatasetController","static","_cachedDataOpts","getMeta","_type","_data","_objectData","_drawStart","_drawCount","enableOptionSharing","supportsDecimation","$context","_syncList","datasetElementType","dataElementType","initialize","linkScales","_stacked","addElements","isPluginEnabled","updateIndex","getDataset","chooseId","xid","xAxisID","yid","yAxisID","rid","rAxisID","iid","iAxisID","vid","vAxisID","getScaleForId","rScale","scaleID","_getOtherScale","reset","_destroy","_dataCheck","iAxisKey","vAxisKey","adata","convertObjectDataToArray","isExtensible","buildOrUpdateElements","resetNewElements","stackChanged","oldStacked","_resyncElements","scopeKeys","datasetScopeKeys","getOptionScopes","createResolver","sorted","parseArrayData","parseObjectData","parsePrimitiveData","isNotInOrderComparedToPrev","labels","getLabels","singleScale","xAxisKey","yAxisKey","getParsed","getDataElement","updateRangeFromParsed","parsedValue","NaN","getMinMax","canStack","otherScale","hidden","createStack","NEGATIVE_INFINITY","otherMin","otherMax","_skip","getAllParsedValues","getMaxOverflow","getLabelAndValue","label","getLabelForValue","_clip","disabled","toClip","defaultClip","resolveDatasetElementOptions","resolveDataElementOptions","dataIndex","raw","createDataContext","createDatasetContext","_resolveElementOptions","elementType","sharing","datasetElementScopeKeys","resolveNamedOptions","_resolveAnimations","transition","datasetAnimationScopeKeys","getSharedOptions","includeOptions","sharedOptions","_animationsDisabled","_getSharedOptions","firstOpts","previouslySharedOptions","updateSharedOptions","updateElement","_setStyle","removeHoverStyle","setHoverStyle","_removeDatasetHoverStyle","_setDatasetHoverStyle","arg1","arg2","numMeta","numData","_insertElements","_removeElements","move","updateElements","removed","_sync","_dataChanges","_onDataPush","arguments","_onDataPop","_onDataShift","_onDataSplice","newCount","_onDataUnshift","Element","tooltipPosition","hasValue","final","tickOpts","determinedMaxTicks","_tickSize","maxScale","_length","maxChart","_maxLength","determineMaxTicks","ticksLimit","maxTicksLimit","majorIndices","enabled","getMajorIndices","numMajorIndices","first","newTicks","spacing","ceil","skipMajors","evenMajorSpacing","diff","getEvenSpacing","factors","calculateSpacing","avgMajorSpacing","majorStart","majorEnd","offsetFromEdge","edge","getTicksLimit","ticksLength","sample","numItems","increment","getPixelForGridLine","offsetGridLines","validIndex","_startPixel","_endPixel","lineValue","getPixelForTick","getTickMarkLength","getTitleHeight","titleAlign","reverseAlign","Scale","super","_margins","paddingTop","paddingBottom","paddingLeft","paddingRight","labelRotation","_range","_gridLineItems","_labelItems","_labelSizes","_longestTextCache","_userMax","_userMin","_suggestedMax","_suggestedMin","_ticksLength","_borderValue","_cache","_dataLimitsCached","init","suggestedMin","suggestedMax","metas","getTicks","xLabels","yLabels","getLabelItems","_computeLabelItems","beforeUpdate","sampleSize","beforeSetDimensions","setDimensions","afterSetDimensions","beforeDataLimits","determineDataLimits","afterDataLimits","beforeBuildTicks","buildTicks","afterBuildTicks","samplingEnabled","_convertTicksToLabels","beforeCalculateLabelRotation","calculateLabelRotation","afterCalculateLabelRotation","afterAutoSkip","beforeFit","fit","afterFit","afterUpdate","startPixel","endPixel","reversePixels","_alignToPixels","alignToPixels","_callHooks","notifyPlugins","beforeTickToLabelConversion","generateTickLabels","afterTickToLabelConversion","numTicks","maxLabelDiagonal","_isVisible","labelSizes","_getLabelSizes","maxLabelWidth","widest","maxLabelHeight","highest","asin","minSize","titleOpts","gridOpts","titleHeight","tickPadding","angleRadians","labelHeight","labelWidth","_calculatePadding","_handleMargins","isRotated","labelsBelowTicks","offsetLeft","offsetRight","isFullSize","_computeLabelSizes","caches","widths","heights","tickFont","fontString","nestedLabel","widestLabelSize","highestLabelSize","_resolveTickFontOptions","valueAt","idx","getValueForPixel","getPixelForDecimal","decimal","getDecimalForPixel","getBasePixel","getBaseValue","createTickContext","optionTicks","rot","_computeGridLineItems","tl","borderOpts","axisWidth","axisHalfWidth","alignBorderValue","borderValue","alignedLineValue","tx1","ty1","tx2","ty2","x1","y1","x2","y2","positionAxisID","limit","step","optsAtIndex","optsAtIndexBorder","lineColor","tickBorderDash","tickBorderDashOffset","tickAndPadding","hTickAndPadding","lineCount","textOffset","_getXAxisLabelAlignment","_getYAxisLabelAlignment","halfCount","tickTextAlign","labelPadding","_computeLabelArea","drawBackground","getLineWidthForValue","findIndex","drawGrid","drawLine","setLineDash","lineDashOffset","drawBorder","lastLineWidth","drawLabels","renderTextOptions","drawTitle","titleX","titleY","titleArgs","tz","gz","bz","axisID","_maxDigits","fontSize","TypedRegistry","isForType","isPrototypeOf","register","parentScope","isIChartComponent","itemDefaults","defaultRoutes","routes","propertyParts","sourceName","sourceScope","routeDefaults","registerDefaults","unregister","Registry","controllers","_typedRegistries","_each","addControllers","addPlugins","addScales","getController","_get","getElement","getPlugin","getScale","removeControllers","removeElements","removePlugins","removeScales","typedRegistry","arg","reg","_getRegistryForType","_exec","itemReg","registry","component","camelMethod","PluginService","_init","notify","hook","_createDescriptors","descriptor","plugin","callCallback","cancelable","invalidate","_oldCache","_notifyStateChanges","localIds","allPlugins","getOpts","pluginOpts","createDescriptors","previousDescriptors","some","pluginScopeKeys","getIndexAxis","datasetDefaults","idMatchesAxis","determineAxis","scaleOptions","getAxisFromDataset","mergeScaleConfig","chartDefaults","configScales","chartIndexAxis","scaleConf","error","boundDs","retrieveAxisFromDatasets","defaultId","getDefaultScaleIDFromAxis","defaultScaleOptions","defaultID","getAxisFromDefaultScaleID","initOptions","initData","keyCache","keysCached","cachedKeys","generate","addIfFound","Config","_config","initConfig","_scopeCache","_resolverCache","clearCache","clear","datasetType","additionalOptionScopes","_cachedScopes","mainScope","resetCache","keyLists","chartOptionScopes","subPrefixes","getResolver","hasFunction","needContext","resolverCache","KNOWN_POSITIONS","positionIsHorizontal","compare2Level","l1","l2","onAnimationsComplete","onComplete","onAnimationProgress","onProgress","getCanvas","getElementById","instances","getChart","moveNumericKeys","intKey","getSizeForArea","field","Chart","invalidatePlugins","userConfig","initialCanvas","existingChart","_options","_aspectRatio","_metasets","_lastEvent","_listeners","_responsiveListeners","_sortedMetasets","_plugins","_hiddenIndices","attached","_doResize","resizeDelay","_initialize","bindEvents","_resizeBeforeDraw","_resize","newSize","newRatio","onResize","render","ensureScalesHaveIDs","axisOptions","buildOrUpdateScales","scaleOpts","updated","isRadial","dposition","dtype","scaleType","hasUpdated","_updateMetasets","_destroyDatasetMeta","_removeUnreferencedMetasets","_dataset","buildOrUpdateControllers","newControllers","order","isDatasetVisible","ControllerClass","_resetElements","animsDisabled","_updateScales","_checkEventBindings","_updateHiddenIndices","_minPadding","_updateLayout","_updateDatasets","_eventHandler","_updateHoverStyles","existingEvents","newEvents","unbindEvents","changes","_getUniformDataChanges","datasetCount","makeSet","changeSet","noArea","_idx","_updateDataset","layers","_drawDatasets","_drawDataset","useClip","getDatasetArea","getElementsAtEventForMode","getVisibleDatasetCount","setDatasetVisibility","toggleDataVisibility","getDataVisibility","_updateVisibility","_stop","destroy","toBase64Image","toDataURL","bindUserEvents","bindResponsiveEvents","_add","_remove","detached","updateHoverStyle","getActiveElements","setActiveElements","activeElements","lastActive","pluginId","replay","hoverOptions","deactivated","activated","inChartArea","eventFilter","_handleEvent","_getActiveElements","isClick","lastEvent","determineLastEvent","abstract","DateAdapterBase","members","formats","startOf","endOf","_adapters","_date","computeMinSampleSize","$bar","visibleMetas","getAllScaleValues","curr","updateMinAndPrev","parseValue","startValue","endValue","barStart","barEnd","_custom","parseFloatBar","parseArrayOrPrimitive","isFloatBar","custom","setBorderSkipped","borderSkipped","borderProps","enableBorderRadius","parseEdge","orig","v2","startEnd","setInflateAmount","inflateAmount","DoughnutController","animateRotate","animateScale","cutout","circumference","legend","generateLabels","fontColor","legendItem","innerRadius","outerRadius","getter","_getRotation","_getCircumference","_getRotationExtents","arcs","getMaxBorderWidth","getMaxOffset","maxSize","chartWeight","_getRingWeight","ratioX","ratioY","startX","startY","endX","endY","calcMax","calcMin","maxX","maxY","minX","minY","getRatioAndOffset","maxRadius","radiusLength","_getVisibleDatasetWeightTotal","calculateTotal","_getRingWeightOffset","_circumference","calculateCircumference","animationOpts","centerX","centerY","metaData","borderAlign","hoverBorderWidth","hoverOffset","ringWeightOffset","PolarAreaController","angleLines","circular","pointLabels","bind","_updateRadius","cutoutPercentage","xCenter","yCenter","datasetStartAngle","getIndexAngle","defaultAngle","countVisibleElements","_computeAngle","getDistanceFromCenterForValue","categoryPercentage","barPercentage","grouped","_index_","_value_","bars","ruler","_getRuler","vpixels","head","_calculateBarValuePixels","ipixels","_calculateBarIndexPixels","_getStacks","currentParsed","iScaleValue","skipNull","find","_getStackCount","_getStackIndex","pixels","barThickness","stackCount","baseValue","minBarLength","actualBase","floating","barSign","halfGrid","maxBarThickness","Infinity","percent","chunk","computeFlexCategoryTraits","thickness","computeFitCategoryTraits","stackIndex","rects","_decimated","animated","maxGapLength","directUpdate","pointsCount","prevParsed","nullData","lastPoint","updateControlPoints","pointPosition","getPointPositionForValue","parseBorderRadius","angleDelta","borderRadius","halfThickness","innerLimit","computeOuterLimit","outerArcLimit","outerStart","outerEnd","innerStart","innerEnd","rThetaToXY","theta","pathArc","pixelMargin","innerR","spacingOffset","avNogSpacingRadius","angleOffset","outerStartAdjustedRadius","outerEndAdjustedRadius","outerStartAdjustedAngle","outerEndAdjustedAngle","innerStartAdjustedRadius","innerEndAdjustedRadius","innerStartAdjustedAngle","innerEndAdjustedAngle","outerMidAdjustedAngle","pCenter","p4","innerMidAdjustedAngle","p8","outerStartX","outerStartY","outerEndX","outerEndY","fullCircles","inner","lineJoin","angleMargin","clipArc","setStyle","lineCap","pathVars","paramsStart","paramsEnd","segmentStart","segmentEnd","outside","pathSegment","lineMethod","stepped","getLineMethod","fastPathSegment","prevX","lastY","avgX","countX","pointIndex","drawX","truncX","_getSegmentMethod","usePath2D","Path2D","path","_path","strokePathWithCache","segmentMethod","strokePathDirect","LineElement","_points","_segments","_pointsUpdated","_interpolate","_getInterpolationMethod","interpolated","hitRadius","getBarBounds","bar","half","skipOrLimit","boundingRects","maxW","maxH","parseBorderWidth","maxR","enableBorder","outer","skipX","skipY","addNormalRectPath","inflateRect","amount","refRect","chartX","chartY","rAdjust","nonZeroBetween","betweenAngles","withinRadius","halfAngle","halfRadius","radiusOffset","drawArc","addRectPath","mouseX","mouseY","inXRange","inYRange","hoverRadius","findOrAddLabel","addedLabels","unshift","addIfString","lastIndexOf","_getLabelForValue","relativeLabelSize","minSpacing","LinearScaleBase","_startValue","_endValue","_valueRange","handleTickRangeOptions","setMin","setMax","minSign","maxSign","getTickLimit","maxTicks","stepSize","computeTickLimit","generationOptions","dataRange","precision","maxDigits","includeBounds","unit","maxSpaces","rmin","rmax","countDefined","niceMin","niceMax","numSpaces","decimalPlaces","generateTicks","LinearScale","log10Floor","changeExponent","isMajor","tickVal","steps","rangeExp","rangeStep","minExp","exp","startExp","lastTick","LogarithmicScale","_zero","getTickBackdropHeight","determineLimits","fitWithPointLabels","_padding","limits","valueCount","_pointLabels","pointLabelOpts","additionalAngle","centerPointLabels","getPointLabelContext","getPointPosition","drawingArea","plFont","textSize","updateLimits","setCenterPoint","_pointLabelItems","itemOpts","extra","createPointLabelItem","isNotOverlapped","buildPointLabelItems","hLimits","vLimits","outerDistance","pointLabelPosition","yForAngle","getTextAlignForAngle","leftForTextAlign","drawPointLabelBox","backdropLeft","backdropTop","backdropWidth","backdropHeight","pathRadiusLine","labelCount","RadialLinearScale","animate","leftMovement","rightMovement","topMovement","bottomMovement","scalingFactor","getValueForDistanceFromCenter","scaledDistance","pointLabel","createPointLabelContext","distanceFromCenter","getBasePosition","getPointLabelPosition","drawPointLabels","gridLineOpts","drawRadiusLine","INTERVALS","millisecond","common","second","minute","hour","day","week","month","quarter","year","UNITS","sorter","adapter","_adapter","parser","isoWeekday","_parseOpts","determineUnitForAutoTicks","minUnit","capacity","interval","MAX_SAFE_INTEGER","addTick","time","timestamps","ticksFromTimestamps","majorUnit","setMajorTicks","TimeScale","adapters","displayFormats","_unit","_majorUnit","_offsets","_normalized","normalized","_applyBounds","_getLabelBounds","getLabelTimestamps","timeOpts","_generate","_getLabelCapacity","determineUnitForFormatting","determineMajorUnit","initOffsets","offsetAfterAutoskip","getDecimalForValue","weekday","hasWeekday","getDataTimestamps","tooltipFormat","datetime","fmt","_tickFormatFunction","minorFormat","majorFormat","offsets","_getLabelSize","ticksOpts","tickLabelWidth","cosRotation","sinRotation","tickFontSize","exampleTime","exampleLabel","prevSource","nextSource","prevTarget","nextTarget","span","_addedLabels","added","_table","_minPos","_tableRange","_getTimestampsForTable","buildLookupTable","BORDER_COLORS","BACKGROUND_COLORS","getBorderColor","getBackgroundColor","getColorizer","colorizeDoughnutDataset","colorizePolarAreaDataset","colorizeDefaultDataset","containsColorsDefinitions","plugin_colors","forceOverride","_args","chartOptions","containsColorDefenition","colorizer","cleanDecimatedDataset","cleanDecimatedData","plugin_decimation","algorithm","beforeElementsUpdate","xAxis","getStartAndCountOfVisiblePointsSimplified","threshold","decimated","samples","bucketWidth","sampledIndex","endIndex","maxAreaPoint","maxArea","nextA","avgY","avgRangeStart","avgRangeEnd","avgRangeLength","rangeOffs","rangeTo","pointAx","pointAy","lttbDecimation","minIndex","maxIndex","startIndex","xMin","dx","lastIndex","intermediateIndex1","intermediateIndex2","minMaxDecimation","_getBounds","_findSegmentEnd","_getEdge","_createBoundaryLine","boundary","linePoints","_pointsFromSegments","_shouldApplyFill","_resolveTarget","propagate","visited","_decodeFill","fillOption","parseFillOption","firstCh","decodeTargetIndex","addPointsBelow","sourcePoint","linesBelow","postponed","findPoint","pointValue","firstValue","lastValue","simpleArc","getLineByIndex","sourcePoints","below","getLinesBelow","_buildStackLine","_getTargetValue","computeCircularBoundary","_getTargetPixel","computeLinearBoundary","computeBoundary","_drawfill","lineOpts","above","clipVertical","doFill","clipY","lineLoop","tpoints","targetSegments","tgt","subBounds","fillSources","fillSource","src","notShape","clipBounds","interpolatedLineTo","targetLoop","interpolatedPoint","afterDatasetsUpdate","$filler","beforeDraw","drawTime","beforeDatasetsDraw","beforeDatasetDraw","getBoxSize","labelOpts","boxHeight","boxWidth","usePointStyle","pointStyleWidth","itemHeight","Legend","_added","legendHitBoxes","_hoveredItem","doughnutMode","legendItems","columnSizes","lineWidths","buildLabels","labelFont","_computeTitleHeight","_fitRows","_fitCols","hitboxes","totalHeight","row","_itemHeight","heightLimit","totalWidth","currentColWidth","currentColHeight","col","legendItemText","calculateItemWidth","fontLineHeight","calculateLegendItemHeight","calculateItemHeight","calculateItemSize","adjustHitBoxes","rtlHelper","hitbox","_draw","defaultColor","halfFontSize","cursor","textDirection","lineDash","drawOptions","SQRT2","yBoxTop","xBoxLeft","drawLegendBox","titleFont","titlePadding","topPaddingPlusHalfFontSize","_getLegendItemAt","hitBox","lh","handleEvent","onLeave","isListened","hoveredItem","sameItem","plugin_legend","_element","afterEvent","ci","useBorderRadius","Title","_drawArgs","fontOpts","plugin_title","titleBlock","createTitle","WeakMap","plugin_subtitle","positioners","average","xSet","eventPosition","nearestElement","tp","pushOrConcat","toPush","splitNewlines","String","createTooltipItem","formattedValue","getTooltipSize","tooltip","body","footer","bodyFont","footerFont","titleLineCount","footerLineCount","bodyLineItemCount","combinedBodyLength","bodyItem","before","after","beforeBody","afterBody","titleSpacing","titleMarginBottom","displayColors","bodySpacing","footerMarginTop","footerSpacing","widthPadding","maxLineWidth","determineXAlign","yAlign","chartWidth","xAlign","caret","caretSize","caretPadding","doesNotFitWithAlign","determineAlignment","determineYAlign","getBackgroundPoint","alignment","paddingAndSize","alignX","alignY","getAlignedX","getBeforeAfterBodyLines","overrideCallbacks","defaultCallbacks","beforeTitle","tooltipItems","afterTitle","beforeLabel","tooltipItem","labelColor","labelTextColor","bodyColor","labelPointStyle","afterLabel","beforeFooter","afterFooter","invokeCallbackWithFallback","Tooltip","opacity","_eventPosition","_size","_cachedAnimations","_tooltipItems","dataPoints","caretX","caretY","labelColors","labelPointStyles","labelTextColors","getTitle","getBeforeBody","getBody","bodyItems","scoped","getAfterBody","getFooter","_createItems","itemSort","positionAndSize","backgroundPoint","external","drawCaret","tooltipPoint","caretPosition","getCaretPosition","x3","y3","ptX","ptY","titleColor","_drawColorBox","colorX","rtlColorX","yOffSet","colorY","multiKeyBackground","outerX","innerX","strokeRect","drawBody","bodyAlign","bodyLineHeight","xLinePadding","fillLineOfText","bodyAlignForCalculation","textColor","drawFooter","footerAlign","footerColor","tooltipSize","quadraticCurveTo","_updateAnimationTarget","animX","animY","_willRender","hasTooltipContent","globalAlpha","positionChanged","_positionChanged","_ignoreReplayEvents","plugin_tooltip","afterInit","afterDraw","helpers","platforms"],"mappings":"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","x_google_ignoreList":[5]} \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.esm.js b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.esm.js new file mode 100644 index 0000000..a1c71c4 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.esm.js @@ -0,0 +1,1351 @@ +/*! + * chartjs-plugin-datalabels v2.2.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2022 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +import { isNullOrUndef, merge, toFont, resolve, toPadding, valueOrDefault, callback, isObject, each } from 'chart.js/helpers'; +import { defaults as defaults$1, ArcElement, PointElement, BarElement } from 'chart.js'; + +var devicePixelRatio = (function() { + if (typeof window !== 'undefined') { + if (window.devicePixelRatio) { + return window.devicePixelRatio; + } + + // devicePixelRatio is undefined on IE10 + // https://stackoverflow.com/a/20204180/8837887 + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/85 + var screen = window.screen; + if (screen) { + return (screen.deviceXDPI || 1) / (screen.logicalXDPI || 1); + } + } + + return 1; +}()); + +var utils = { + // @todo move this in Chart.helpers.toTextLines + toTextLines: function(inputs) { + var lines = []; + var input; + + inputs = [].concat(inputs); + while (inputs.length) { + input = inputs.pop(); + if (typeof input === 'string') { + lines.unshift.apply(lines, input.split('\n')); + } else if (Array.isArray(input)) { + inputs.push.apply(inputs, input); + } else if (!isNullOrUndef(inputs)) { + lines.unshift('' + input); + } + } + + return lines; + }, + + // @todo move this in Chart.helpers.canvas.textSize + // @todo cache calls of measureText if font doesn't change?! + textSize: function(ctx, lines, font) { + var items = [].concat(lines); + var ilen = items.length; + var prev = ctx.font; + var width = 0; + var i; + + ctx.font = font.string; + + for (i = 0; i < ilen; ++i) { + width = Math.max(ctx.measureText(items[i]).width, width); + } + + ctx.font = prev; + + return { + height: ilen * font.lineHeight, + width: width + }; + }, + + /** + * Returns value bounded by min and max. This is equivalent to max(min, min(value, max)). + * @todo move this method in Chart.helpers.bound + * https://doc.qt.io/qt-5/qtglobal.html#qBound + */ + bound: function(min, value, max) { + return Math.max(min, Math.min(value, max)); + }, + + /** + * Returns an array of pair [value, state] where state is: + * * -1: value is only in a0 (removed) + * * 1: value is only in a1 (added) + */ + arrayDiff: function(a0, a1) { + var prev = a0.slice(); + var updates = []; + var i, j, ilen, v; + + for (i = 0, ilen = a1.length; i < ilen; ++i) { + v = a1[i]; + j = prev.indexOf(v); + + if (j === -1) { + updates.push([v, 1]); + } else { + prev.splice(j, 1); + } + } + + for (i = 0, ilen = prev.length; i < ilen; ++i) { + updates.push([prev[i], -1]); + } + + return updates; + }, + + /** + * https://github.com/chartjs/chartjs-plugin-datalabels/issues/70 + */ + rasterize: function(v) { + return Math.round(v * devicePixelRatio) / devicePixelRatio; + } +}; + +function orient(point, origin) { + var x0 = origin.x; + var y0 = origin.y; + + if (x0 === null) { + return {x: 0, y: -1}; + } + if (y0 === null) { + return {x: 1, y: 0}; + } + + var dx = point.x - x0; + var dy = point.y - y0; + var ln = Math.sqrt(dx * dx + dy * dy); + + return { + x: ln ? dx / ln : 0, + y: ln ? dy / ln : -1 + }; +} + +function aligned(x, y, vx, vy, align) { + switch (align) { + case 'center': + vx = vy = 0; + break; + case 'bottom': + vx = 0; + vy = 1; + break; + case 'right': + vx = 1; + vy = 0; + break; + case 'left': + vx = -1; + vy = 0; + break; + case 'top': + vx = 0; + vy = -1; + break; + case 'start': + vx = -vx; + vy = -vy; + break; + case 'end': + // keep natural orientation + break; + default: + // clockwise rotation (in degree) + align *= (Math.PI / 180); + vx = Math.cos(align); + vy = Math.sin(align); + break; + } + + return { + x: x, + y: y, + vx: vx, + vy: vy + }; +} + +// Line clipping (Cohen–Sutherland algorithm) +// https://en.wikipedia.org/wiki/Cohen–Sutherland_algorithm + +var R_INSIDE = 0; +var R_LEFT = 1; +var R_RIGHT = 2; +var R_BOTTOM = 4; +var R_TOP = 8; + +function region(x, y, rect) { + var res = R_INSIDE; + + if (x < rect.left) { + res |= R_LEFT; + } else if (x > rect.right) { + res |= R_RIGHT; + } + if (y < rect.top) { + res |= R_TOP; + } else if (y > rect.bottom) { + res |= R_BOTTOM; + } + + return res; +} + +function clipped(segment, area) { + var x0 = segment.x0; + var y0 = segment.y0; + var x1 = segment.x1; + var y1 = segment.y1; + var r0 = region(x0, y0, area); + var r1 = region(x1, y1, area); + var r, x, y; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (!(r0 | r1) || (r0 & r1)) { + // both points inside or on the same side: no clipping + break; + } + + // at least one point is outside + r = r0 || r1; + + if (r & R_TOP) { + x = x0 + (x1 - x0) * (area.top - y0) / (y1 - y0); + y = area.top; + } else if (r & R_BOTTOM) { + x = x0 + (x1 - x0) * (area.bottom - y0) / (y1 - y0); + y = area.bottom; + } else if (r & R_RIGHT) { + y = y0 + (y1 - y0) * (area.right - x0) / (x1 - x0); + x = area.right; + } else if (r & R_LEFT) { + y = y0 + (y1 - y0) * (area.left - x0) / (x1 - x0); + x = area.left; + } + + if (r === r0) { + x0 = x; + y0 = y; + r0 = region(x0, y0, area); + } else { + x1 = x; + y1 = y; + r1 = region(x1, y1, area); + } + } + + return { + x0: x0, + x1: x1, + y0: y0, + y1: y1 + }; +} + +function compute$1(range, config) { + var anchor = config.anchor; + var segment = range; + var x, y; + + if (config.clamp) { + segment = clipped(segment, config.area); + } + + if (anchor === 'start') { + x = segment.x0; + y = segment.y0; + } else if (anchor === 'end') { + x = segment.x1; + y = segment.y1; + } else { + x = (segment.x0 + segment.x1) / 2; + y = (segment.y0 + segment.y1) / 2; + } + + return aligned(x, y, range.vx, range.vy, config.align); +} + +var positioners = { + arc: function(el, config) { + var angle = (el.startAngle + el.endAngle) / 2; + var vx = Math.cos(angle); + var vy = Math.sin(angle); + var r0 = el.innerRadius; + var r1 = el.outerRadius; + + return compute$1({ + x0: el.x + vx * r0, + y0: el.y + vy * r0, + x1: el.x + vx * r1, + y1: el.y + vy * r1, + vx: vx, + vy: vy + }, config); + }, + + point: function(el, config) { + var v = orient(el, config.origin); + var rx = v.x * el.options.radius; + var ry = v.y * el.options.radius; + + return compute$1({ + x0: el.x - rx, + y0: el.y - ry, + x1: el.x + rx, + y1: el.y + ry, + vx: v.x, + vy: v.y + }, config); + }, + + bar: function(el, config) { + var v = orient(el, config.origin); + var x = el.x; + var y = el.y; + var sx = 0; + var sy = 0; + + if (el.horizontal) { + x = Math.min(el.x, el.base); + sx = Math.abs(el.base - el.x); + } else { + y = Math.min(el.y, el.base); + sy = Math.abs(el.base - el.y); + } + + return compute$1({ + x0: x, + y0: y + sy, + x1: x + sx, + y1: y, + vx: v.x, + vy: v.y + }, config); + }, + + fallback: function(el, config) { + var v = orient(el, config.origin); + + return compute$1({ + x0: el.x, + y0: el.y, + x1: el.x + (el.width || 0), + y1: el.y + (el.height || 0), + vx: v.x, + vy: v.y + }, config); + } +}; + +var rasterize = utils.rasterize; + +function boundingRects(model) { + var borderWidth = model.borderWidth || 0; + var padding = model.padding; + var th = model.size.height; + var tw = model.size.width; + var tx = -tw / 2; + var ty = -th / 2; + + return { + frame: { + x: tx - padding.left - borderWidth, + y: ty - padding.top - borderWidth, + w: tw + padding.width + borderWidth * 2, + h: th + padding.height + borderWidth * 2 + }, + text: { + x: tx, + y: ty, + w: tw, + h: th + } + }; +} + +function getScaleOrigin(el, context) { + var scale = context.chart.getDatasetMeta(context.datasetIndex).vScale; + + if (!scale) { + return null; + } + + if (scale.xCenter !== undefined && scale.yCenter !== undefined) { + return {x: scale.xCenter, y: scale.yCenter}; + } + + var pixel = scale.getBasePixel(); + return el.horizontal ? + {x: pixel, y: null} : + {x: null, y: pixel}; +} + +function getPositioner(el) { + if (el instanceof ArcElement) { + return positioners.arc; + } + if (el instanceof PointElement) { + return positioners.point; + } + if (el instanceof BarElement) { + return positioners.bar; + } + return positioners.fallback; +} + +function drawRoundedRect(ctx, x, y, w, h, radius) { + var HALF_PI = Math.PI / 2; + + if (radius) { + var r = Math.min(radius, h / 2, w / 2); + var left = x + r; + var top = y + r; + var right = x + w - r; + var bottom = y + h - r; + + ctx.moveTo(x, top); + if (left < right && top < bottom) { + ctx.arc(left, top, r, -Math.PI, -HALF_PI); + ctx.arc(right, top, r, -HALF_PI, 0); + ctx.arc(right, bottom, r, 0, HALF_PI); + ctx.arc(left, bottom, r, HALF_PI, Math.PI); + } else if (left < right) { + ctx.moveTo(left, y); + ctx.arc(right, top, r, -HALF_PI, HALF_PI); + ctx.arc(left, top, r, HALF_PI, Math.PI + HALF_PI); + } else if (top < bottom) { + ctx.arc(left, top, r, -Math.PI, 0); + ctx.arc(left, bottom, r, 0, Math.PI); + } else { + ctx.arc(left, top, r, -Math.PI, Math.PI); + } + ctx.closePath(); + ctx.moveTo(x, y); + } else { + ctx.rect(x, y, w, h); + } +} + +function drawFrame(ctx, rect, model) { + var bgColor = model.backgroundColor; + var borderColor = model.borderColor; + var borderWidth = model.borderWidth; + + if (!bgColor && (!borderColor || !borderWidth)) { + return; + } + + ctx.beginPath(); + + drawRoundedRect( + ctx, + rasterize(rect.x) + borderWidth / 2, + rasterize(rect.y) + borderWidth / 2, + rasterize(rect.w) - borderWidth, + rasterize(rect.h) - borderWidth, + model.borderRadius); + + ctx.closePath(); + + if (bgColor) { + ctx.fillStyle = bgColor; + ctx.fill(); + } + + if (borderColor && borderWidth) { + ctx.strokeStyle = borderColor; + ctx.lineWidth = borderWidth; + ctx.lineJoin = 'miter'; + ctx.stroke(); + } +} + +function textGeometry(rect, align, font) { + var h = font.lineHeight; + var w = rect.w; + var x = rect.x; + var y = rect.y + h / 2; + + if (align === 'center') { + x += w / 2; + } else if (align === 'end' || align === 'right') { + x += w; + } + + return { + h: h, + w: w, + x: x, + y: y + }; +} + +function drawTextLine(ctx, text, cfg) { + var shadow = ctx.shadowBlur; + var stroked = cfg.stroked; + var x = rasterize(cfg.x); + var y = rasterize(cfg.y); + var w = rasterize(cfg.w); + + if (stroked) { + ctx.strokeText(text, x, y, w); + } + + if (cfg.filled) { + if (shadow && stroked) { + // Prevent drawing shadow on both the text stroke and fill, so + // if the text is stroked, remove the shadow for the text fill. + ctx.shadowBlur = 0; + } + + ctx.fillText(text, x, y, w); + + if (shadow && stroked) { + ctx.shadowBlur = shadow; + } + } +} + +function drawText(ctx, lines, rect, model) { + var align = model.textAlign; + var color = model.color; + var filled = !!color; + var font = model.font; + var ilen = lines.length; + var strokeColor = model.textStrokeColor; + var strokeWidth = model.textStrokeWidth; + var stroked = strokeColor && strokeWidth; + var i; + + if (!ilen || (!filled && !stroked)) { + return; + } + + // Adjust coordinates based on text alignment and line height + rect = textGeometry(rect, align, font); + + ctx.font = font.string; + ctx.textAlign = align; + ctx.textBaseline = 'middle'; + ctx.shadowBlur = model.textShadowBlur; + ctx.shadowColor = model.textShadowColor; + + if (filled) { + ctx.fillStyle = color; + } + if (stroked) { + ctx.lineJoin = 'round'; + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = strokeColor; + } + + for (i = 0, ilen = lines.length; i < ilen; ++i) { + drawTextLine(ctx, lines[i], { + stroked: stroked, + filled: filled, + w: rect.w, + x: rect.x, + y: rect.y + rect.h * i + }); + } +} + +var Label = function(config, ctx, el, index) { + var me = this; + + me._config = config; + me._index = index; + me._model = null; + me._rects = null; + me._ctx = ctx; + me._el = el; +}; + +merge(Label.prototype, { + /** + * @private + */ + _modelize: function(display, lines, config, context) { + var me = this; + var index = me._index; + var font = toFont(resolve([config.font, {}], context, index)); + var color = resolve([config.color, defaults$1.color], context, index); + + return { + align: resolve([config.align, 'center'], context, index), + anchor: resolve([config.anchor, 'center'], context, index), + area: context.chart.chartArea, + backgroundColor: resolve([config.backgroundColor, null], context, index), + borderColor: resolve([config.borderColor, null], context, index), + borderRadius: resolve([config.borderRadius, 0], context, index), + borderWidth: resolve([config.borderWidth, 0], context, index), + clamp: resolve([config.clamp, false], context, index), + clip: resolve([config.clip, false], context, index), + color: color, + display: display, + font: font, + lines: lines, + offset: resolve([config.offset, 4], context, index), + opacity: resolve([config.opacity, 1], context, index), + origin: getScaleOrigin(me._el, context), + padding: toPadding(resolve([config.padding, 4], context, index)), + positioner: getPositioner(me._el), + rotation: resolve([config.rotation, 0], context, index) * (Math.PI / 180), + size: utils.textSize(me._ctx, lines, font), + textAlign: resolve([config.textAlign, 'start'], context, index), + textShadowBlur: resolve([config.textShadowBlur, 0], context, index), + textShadowColor: resolve([config.textShadowColor, color], context, index), + textStrokeColor: resolve([config.textStrokeColor, color], context, index), + textStrokeWidth: resolve([config.textStrokeWidth, 0], context, index) + }; + }, + + update: function(context) { + var me = this; + var model = null; + var rects = null; + var index = me._index; + var config = me._config; + var value, label, lines; + + // We first resolve the display option (separately) to avoid computing + // other options in case the label is hidden (i.e. display: false). + var display = resolve([config.display, true], context, index); + + if (display) { + value = context.dataset.data[index]; + label = valueOrDefault(callback(config.formatter, [value, context]), value); + lines = isNullOrUndef(label) ? [] : utils.toTextLines(label); + + if (lines.length) { + model = me._modelize(display, lines, config, context); + rects = boundingRects(model); + } + } + + me._model = model; + me._rects = rects; + }, + + geometry: function() { + return this._rects ? this._rects.frame : {}; + }, + + rotation: function() { + return this._model ? this._model.rotation : 0; + }, + + visible: function() { + return this._model && this._model.opacity; + }, + + model: function() { + return this._model; + }, + + draw: function(chart, center) { + var me = this; + var ctx = chart.ctx; + var model = me._model; + var rects = me._rects; + var area; + + if (!this.visible()) { + return; + } + + ctx.save(); + + if (model.clip) { + area = model.area; + ctx.beginPath(); + ctx.rect( + area.left, + area.top, + area.right - area.left, + area.bottom - area.top); + ctx.clip(); + } + + ctx.globalAlpha = utils.bound(0, model.opacity, 1); + ctx.translate(rasterize(center.x), rasterize(center.y)); + ctx.rotate(model.rotation); + + drawFrame(ctx, rects.frame, model); + drawText(ctx, model.lines, rects.text, model); + + ctx.restore(); + } +}); + +var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; // eslint-disable-line es/no-number-minsafeinteger +var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line es/no-number-maxsafeinteger + +function rotated(point, center, angle) { + var cos = Math.cos(angle); + var sin = Math.sin(angle); + var cx = center.x; + var cy = center.y; + + return { + x: cx + cos * (point.x - cx) - sin * (point.y - cy), + y: cy + sin * (point.x - cx) + cos * (point.y - cy) + }; +} + +function projected(points, axis) { + var min = MAX_INTEGER; + var max = MIN_INTEGER; + var origin = axis.origin; + var i, pt, vx, vy, dp; + + for (i = 0; i < points.length; ++i) { + pt = points[i]; + vx = pt.x - origin.x; + vy = pt.y - origin.y; + dp = axis.vx * vx + axis.vy * vy; + min = Math.min(min, dp); + max = Math.max(max, dp); + } + + return { + min: min, + max: max + }; +} + +function toAxis(p0, p1) { + var vx = p1.x - p0.x; + var vy = p1.y - p0.y; + var ln = Math.sqrt(vx * vx + vy * vy); + + return { + vx: (p1.x - p0.x) / ln, + vy: (p1.y - p0.y) / ln, + origin: p0, + ln: ln + }; +} + +var HitBox = function() { + this._rotation = 0; + this._rect = { + x: 0, + y: 0, + w: 0, + h: 0 + }; +}; + +merge(HitBox.prototype, { + center: function() { + var r = this._rect; + return { + x: r.x + r.w / 2, + y: r.y + r.h / 2 + }; + }, + + update: function(center, rect, rotation) { + this._rotation = rotation; + this._rect = { + x: rect.x + center.x, + y: rect.y + center.y, + w: rect.w, + h: rect.h + }; + }, + + contains: function(point) { + var me = this; + var margin = 1; + var rect = me._rect; + + point = rotated(point, me.center(), -me._rotation); + + return !(point.x < rect.x - margin + || point.y < rect.y - margin + || point.x > rect.x + rect.w + margin * 2 + || point.y > rect.y + rect.h + margin * 2); + }, + + // Separating Axis Theorem + // https://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169 + intersects: function(other) { + var r0 = this._points(); + var r1 = other._points(); + var axes = [ + toAxis(r0[0], r0[1]), + toAxis(r0[0], r0[3]) + ]; + var i, pr0, pr1; + + if (this._rotation !== other._rotation) { + // Only separate with r1 axis if the rotation is different, + // else it's enough to separate r0 and r1 with r0 axis only! + axes.push( + toAxis(r1[0], r1[1]), + toAxis(r1[0], r1[3]) + ); + } + + for (i = 0; i < axes.length; ++i) { + pr0 = projected(r0, axes[i]); + pr1 = projected(r1, axes[i]); + + if (pr0.max < pr1.min || pr1.max < pr0.min) { + return false; + } + } + + return true; + }, + + /** + * @private + */ + _points: function() { + var me = this; + var rect = me._rect; + var angle = me._rotation; + var center = me.center(); + + return [ + rotated({x: rect.x, y: rect.y}, center, angle), + rotated({x: rect.x + rect.w, y: rect.y}, center, angle), + rotated({x: rect.x + rect.w, y: rect.y + rect.h}, center, angle), + rotated({x: rect.x, y: rect.y + rect.h}, center, angle) + ]; + } +}); + +function coordinates(el, model, geometry) { + var point = model.positioner(el, model); + var vx = point.vx; + var vy = point.vy; + + if (!vx && !vy) { + // if aligned center, we don't want to offset the center point + return {x: point.x, y: point.y}; + } + + var w = geometry.w; + var h = geometry.h; + + // take in account the label rotation + var rotation = model.rotation; + var dx = Math.abs(w / 2 * Math.cos(rotation)) + Math.abs(h / 2 * Math.sin(rotation)); + var dy = Math.abs(w / 2 * Math.sin(rotation)) + Math.abs(h / 2 * Math.cos(rotation)); + + // scale the unit vector (vx, vy) to get at least dx or dy equal to + // w or h respectively (else we would calculate the distance to the + // ellipse inscribed in the bounding rect) + var vs = 1 / Math.max(Math.abs(vx), Math.abs(vy)); + dx *= vx * vs; + dy *= vy * vs; + + // finally, include the explicit offset + dx += model.offset * vx; + dy += model.offset * vy; + + return { + x: point.x + dx, + y: point.y + dy + }; +} + +function collide(labels, collider) { + var i, j, s0, s1; + + // IMPORTANT Iterate in the reverse order since items at the end of the + // list have an higher weight/priority and thus should be less impacted + // by the overlapping strategy. + + for (i = labels.length - 1; i >= 0; --i) { + s0 = labels[i].$layout; + + for (j = i - 1; j >= 0 && s0._visible; --j) { + s1 = labels[j].$layout; + + if (s1._visible && s0._box.intersects(s1._box)) { + collider(s0, s1); + } + } + } + + return labels; +} + +function compute(labels) { + var i, ilen, label, state, geometry, center, proxy; + + // Initialize labels for overlap detection + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + state = label.$layout; + + if (state._visible) { + // Chart.js 3 removed el._model in favor of getProps(), making harder to + // abstract reading values in positioners. Also, using string arrays to + // read values (i.e. var {a,b,c} = el.getProps(["a","b","c"])) would make + // positioners inefficient in the normal case (i.e. not the final values) + // and the code a bit ugly, so let's use a Proxy instead. + proxy = new Proxy(label._el, {get: (el, p) => el.getProps([p], true)[p]}); + + geometry = label.geometry(); + center = coordinates(proxy, label.model(), geometry); + state._box.update(center, geometry, label.rotation()); + } + } + + // Auto hide overlapping labels + return collide(labels, function(s0, s1) { + var h0 = s0._hidable; + var h1 = s1._hidable; + + if ((h0 && h1) || h1) { + s1._visible = false; + } else if (h0) { + s0._visible = false; + } + }); +} + +var layout = { + prepare: function(datasets) { + var labels = []; + var i, j, ilen, jlen, label; + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + for (j = 0, jlen = datasets[i].length; j < jlen; ++j) { + label = datasets[i][j]; + labels.push(label); + label.$layout = { + _box: new HitBox(), + _hidable: false, + _visible: true, + _set: i, + _idx: label._index + }; + } + } + + // TODO New `z` option: labels with a higher z-index are drawn + // of top of the ones with a lower index. Lowest z-index labels + // are also discarded first when hiding overlapping labels. + labels.sort(function(a, b) { + var sa = a.$layout; + var sb = b.$layout; + + return sa._idx === sb._idx + ? sb._set - sa._set + : sb._idx - sa._idx; + }); + + this.update(labels); + + return labels; + }, + + update: function(labels) { + var dirty = false; + var i, ilen, label, model, state; + + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + model = label.model(); + state = label.$layout; + state._hidable = model && model.display === 'auto'; + state._visible = label.visible(); + dirty |= state._hidable; + } + + if (dirty) { + compute(labels); + } + }, + + lookup: function(labels, point) { + var i, state; + + // IMPORTANT Iterate in the reverse order since items at the end of + // the list have an higher z-index, thus should be picked first. + + for (i = labels.length - 1; i >= 0; --i) { + state = labels[i].$layout; + + if (state && state._visible && state._box.contains(point)) { + return labels[i]; + } + } + + return null; + }, + + draw: function(chart, labels) { + var i, ilen, label, state, geometry, center; + + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + state = label.$layout; + + if (state._visible) { + geometry = label.geometry(); + center = coordinates(label._el, label.model(), geometry); + state._box.update(center, geometry, label.rotation()); + label.draw(chart, center); + } + } + } +}; + +var formatter = function(value) { + if (isNullOrUndef(value)) { + return null; + } + + var label = value; + var keys, klen, k; + if (isObject(value)) { + if (!isNullOrUndef(value.label)) { + label = value.label; + } else if (!isNullOrUndef(value.r)) { + label = value.r; + } else { + label = ''; + keys = Object.keys(value); + for (k = 0, klen = keys.length; k < klen; ++k) { + label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]]; + } + } + } + + return '' + label; +}; + +/** + * IMPORTANT: make sure to also update tests and TypeScript definition + * files (`/test/specs/defaults.spec.js` and `/types/options.d.ts`) + */ + +var defaults = { + align: 'center', + anchor: 'center', + backgroundColor: null, + borderColor: null, + borderRadius: 0, + borderWidth: 0, + clamp: false, + clip: false, + color: undefined, + display: true, + font: { + family: undefined, + lineHeight: 1.2, + size: undefined, + style: undefined, + weight: null + }, + formatter: formatter, + labels: undefined, + listeners: {}, + offset: 4, + opacity: 1, + padding: { + top: 4, + right: 4, + bottom: 4, + left: 4 + }, + rotation: 0, + textAlign: 'start', + textStrokeColor: undefined, + textStrokeWidth: 0, + textShadowBlur: 0, + textShadowColor: undefined +}; + +/** + * @see https://github.com/chartjs/Chart.js/issues/4176 + */ + +var EXPANDO_KEY = '$datalabels'; +var DEFAULT_KEY = '$default'; + +function configure(dataset, options) { + var override = dataset.datalabels; + var listeners = {}; + var configs = []; + var labels, keys; + + if (override === false) { + return null; + } + if (override === true) { + override = {}; + } + + options = merge({}, [options, override]); + labels = options.labels || {}; + keys = Object.keys(labels); + delete options.labels; + + if (keys.length) { + keys.forEach(function(key) { + if (labels[key]) { + configs.push(merge({}, [ + options, + labels[key], + {_key: key} + ])); + } + }); + } else { + // Default label if no "named" label defined. + configs.push(options); + } + + // listeners: {: {: }} + listeners = configs.reduce(function(target, config) { + each(config.listeners || {}, function(fn, event) { + target[event] = target[event] || {}; + target[event][config._key || DEFAULT_KEY] = fn; + }); + + delete config.listeners; + return target; + }, {}); + + return { + labels: configs, + listeners: listeners + }; +} + +function dispatchEvent(chart, listeners, label, event) { + if (!listeners) { + return; + } + + var context = label.$context; + var groups = label.$groups; + var callback$1; + + if (!listeners[groups._set]) { + return; + } + + callback$1 = listeners[groups._set][groups._key]; + if (!callback$1) { + return; + } + + if (callback(callback$1, [context, event]) === true) { + // Users are allowed to tweak the given context by injecting values that can be + // used in scriptable options to display labels differently based on the current + // event (e.g. highlight an hovered label). That's why we update the label with + // the output context and schedule a new chart render by setting it dirty. + chart[EXPANDO_KEY]._dirty = true; + label.update(context); + } +} + +function dispatchMoveEvents(chart, listeners, previous, label, event) { + var enter, leave; + + if (!previous && !label) { + return; + } + + if (!previous) { + enter = true; + } else if (!label) { + leave = true; + } else if (previous !== label) { + leave = enter = true; + } + + if (leave) { + dispatchEvent(chart, listeners.leave, previous, event); + } + if (enter) { + dispatchEvent(chart, listeners.enter, label, event); + } +} + +function handleMoveEvents(chart, event) { + var expando = chart[EXPANDO_KEY]; + var listeners = expando._listeners; + var previous, label; + + if (!listeners.enter && !listeners.leave) { + return; + } + + if (event.type === 'mousemove') { + label = layout.lookup(expando._labels, event); + } else if (event.type !== 'mouseout') { + return; + } + + previous = expando._hovered; + expando._hovered = label; + dispatchMoveEvents(chart, listeners, previous, label, event); +} + +function handleClickEvents(chart, event) { + var expando = chart[EXPANDO_KEY]; + var handlers = expando._listeners.click; + var label = handlers && layout.lookup(expando._labels, event); + if (label) { + dispatchEvent(chart, handlers, label, event); + } +} + +var plugin = { + id: 'datalabels', + + defaults: defaults, + + beforeInit: function(chart) { + chart[EXPANDO_KEY] = { + _actives: [] + }; + }, + + beforeUpdate: function(chart) { + var expando = chart[EXPANDO_KEY]; + expando._listened = false; + expando._listeners = {}; // {: {: {: }}} + expando._datasets = []; // per dataset labels: [Label[]] + expando._labels = []; // layouted labels: Label[] + }, + + afterDatasetUpdate: function(chart, args, options) { + var datasetIndex = args.index; + var expando = chart[EXPANDO_KEY]; + var labels = expando._datasets[datasetIndex] = []; + var visible = chart.isDatasetVisible(datasetIndex); + var dataset = chart.data.datasets[datasetIndex]; + var config = configure(dataset, options); + var elements = args.meta.data || []; + var ctx = chart.ctx; + var i, j, ilen, jlen, cfg, key, el, label; + + ctx.save(); + + for (i = 0, ilen = elements.length; i < ilen; ++i) { + el = elements[i]; + el[EXPANDO_KEY] = []; + + if (visible && el && chart.getDataVisibility(i) && !el.skip) { + for (j = 0, jlen = config.labels.length; j < jlen; ++j) { + cfg = config.labels[j]; + key = cfg._key; + + label = new Label(cfg, ctx, el, i); + label.$groups = { + _set: datasetIndex, + _key: key || DEFAULT_KEY + }; + label.$context = { + active: false, + chart: chart, + dataIndex: i, + dataset: dataset, + datasetIndex: datasetIndex + }; + + label.update(label.$context); + el[EXPANDO_KEY].push(label); + labels.push(label); + } + } + } + + ctx.restore(); + + // Store listeners at the chart level and per event type to optimize + // cases where no listeners are registered for a specific event. + merge(expando._listeners, config.listeners, { + merger: function(event, target, source) { + target[event] = target[event] || {}; + target[event][args.index] = source[event]; + expando._listened = true; + } + }); + }, + + afterUpdate: function(chart) { + chart[EXPANDO_KEY]._labels = layout.prepare(chart[EXPANDO_KEY]._datasets); + }, + + // Draw labels on top of all dataset elements + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/29 + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/32 + afterDatasetsDraw: function(chart) { + layout.draw(chart, chart[EXPANDO_KEY]._labels); + }, + + beforeEvent: function(chart, args) { + // If there is no listener registered for this chart, `listened` will be false, + // meaning we can immediately ignore the incoming event and avoid useless extra + // computation for users who don't implement label interactions. + if (chart[EXPANDO_KEY]._listened) { + var event = args.event; + switch (event.type) { + case 'mousemove': + case 'mouseout': + handleMoveEvents(chart, event); + break; + case 'click': + handleClickEvents(chart, event); + break; + } + } + }, + + afterEvent: function(chart) { + var expando = chart[EXPANDO_KEY]; + var previous = expando._actives; + var actives = expando._actives = chart.getActiveElements(); + var updates = utils.arrayDiff(previous, actives); + var i, ilen, j, jlen, update, label, labels; + + for (i = 0, ilen = updates.length; i < ilen; ++i) { + update = updates[i]; + if (update[1]) { + labels = update[0].element[EXPANDO_KEY] || []; + for (j = 0, jlen = labels.length; j < jlen; ++j) { + label = labels[j]; + label.$context.active = (update[1] === 1); + label.update(label.$context); + } + } + } + + if (expando._dirty || updates.length) { + layout.update(expando._labels); + chart.render(); + } + + delete expando._dirty; + } +}; + +export { plugin as default }; diff --git a/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.js b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.js new file mode 100644 index 0000000..796524d --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.js @@ -0,0 +1,1356 @@ +/*! + * chartjs-plugin-datalabels v2.2.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2022 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js/helpers'), require('chart.js')) : +typeof define === 'function' && define.amd ? define(['chart.js/helpers', 'chart.js'], factory) : +(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ChartDataLabels = factory(global.Chart.helpers, global.Chart)); +})(this, (function (helpers, chart_js) { 'use strict'; + +var devicePixelRatio = (function() { + if (typeof window !== 'undefined') { + if (window.devicePixelRatio) { + return window.devicePixelRatio; + } + + // devicePixelRatio is undefined on IE10 + // https://stackoverflow.com/a/20204180/8837887 + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/85 + var screen = window.screen; + if (screen) { + return (screen.deviceXDPI || 1) / (screen.logicalXDPI || 1); + } + } + + return 1; +}()); + +var utils = { + // @todo move this in Chart.helpers.toTextLines + toTextLines: function(inputs) { + var lines = []; + var input; + + inputs = [].concat(inputs); + while (inputs.length) { + input = inputs.pop(); + if (typeof input === 'string') { + lines.unshift.apply(lines, input.split('\n')); + } else if (Array.isArray(input)) { + inputs.push.apply(inputs, input); + } else if (!helpers.isNullOrUndef(inputs)) { + lines.unshift('' + input); + } + } + + return lines; + }, + + // @todo move this in Chart.helpers.canvas.textSize + // @todo cache calls of measureText if font doesn't change?! + textSize: function(ctx, lines, font) { + var items = [].concat(lines); + var ilen = items.length; + var prev = ctx.font; + var width = 0; + var i; + + ctx.font = font.string; + + for (i = 0; i < ilen; ++i) { + width = Math.max(ctx.measureText(items[i]).width, width); + } + + ctx.font = prev; + + return { + height: ilen * font.lineHeight, + width: width + }; + }, + + /** + * Returns value bounded by min and max. This is equivalent to max(min, min(value, max)). + * @todo move this method in Chart.helpers.bound + * https://doc.qt.io/qt-5/qtglobal.html#qBound + */ + bound: function(min, value, max) { + return Math.max(min, Math.min(value, max)); + }, + + /** + * Returns an array of pair [value, state] where state is: + * * -1: value is only in a0 (removed) + * * 1: value is only in a1 (added) + */ + arrayDiff: function(a0, a1) { + var prev = a0.slice(); + var updates = []; + var i, j, ilen, v; + + for (i = 0, ilen = a1.length; i < ilen; ++i) { + v = a1[i]; + j = prev.indexOf(v); + + if (j === -1) { + updates.push([v, 1]); + } else { + prev.splice(j, 1); + } + } + + for (i = 0, ilen = prev.length; i < ilen; ++i) { + updates.push([prev[i], -1]); + } + + return updates; + }, + + /** + * https://github.com/chartjs/chartjs-plugin-datalabels/issues/70 + */ + rasterize: function(v) { + return Math.round(v * devicePixelRatio) / devicePixelRatio; + } +}; + +function orient(point, origin) { + var x0 = origin.x; + var y0 = origin.y; + + if (x0 === null) { + return {x: 0, y: -1}; + } + if (y0 === null) { + return {x: 1, y: 0}; + } + + var dx = point.x - x0; + var dy = point.y - y0; + var ln = Math.sqrt(dx * dx + dy * dy); + + return { + x: ln ? dx / ln : 0, + y: ln ? dy / ln : -1 + }; +} + +function aligned(x, y, vx, vy, align) { + switch (align) { + case 'center': + vx = vy = 0; + break; + case 'bottom': + vx = 0; + vy = 1; + break; + case 'right': + vx = 1; + vy = 0; + break; + case 'left': + vx = -1; + vy = 0; + break; + case 'top': + vx = 0; + vy = -1; + break; + case 'start': + vx = -vx; + vy = -vy; + break; + case 'end': + // keep natural orientation + break; + default: + // clockwise rotation (in degree) + align *= (Math.PI / 180); + vx = Math.cos(align); + vy = Math.sin(align); + break; + } + + return { + x: x, + y: y, + vx: vx, + vy: vy + }; +} + +// Line clipping (Cohen–Sutherland algorithm) +// https://en.wikipedia.org/wiki/Cohen–Sutherland_algorithm + +var R_INSIDE = 0; +var R_LEFT = 1; +var R_RIGHT = 2; +var R_BOTTOM = 4; +var R_TOP = 8; + +function region(x, y, rect) { + var res = R_INSIDE; + + if (x < rect.left) { + res |= R_LEFT; + } else if (x > rect.right) { + res |= R_RIGHT; + } + if (y < rect.top) { + res |= R_TOP; + } else if (y > rect.bottom) { + res |= R_BOTTOM; + } + + return res; +} + +function clipped(segment, area) { + var x0 = segment.x0; + var y0 = segment.y0; + var x1 = segment.x1; + var y1 = segment.y1; + var r0 = region(x0, y0, area); + var r1 = region(x1, y1, area); + var r, x, y; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (!(r0 | r1) || (r0 & r1)) { + // both points inside or on the same side: no clipping + break; + } + + // at least one point is outside + r = r0 || r1; + + if (r & R_TOP) { + x = x0 + (x1 - x0) * (area.top - y0) / (y1 - y0); + y = area.top; + } else if (r & R_BOTTOM) { + x = x0 + (x1 - x0) * (area.bottom - y0) / (y1 - y0); + y = area.bottom; + } else if (r & R_RIGHT) { + y = y0 + (y1 - y0) * (area.right - x0) / (x1 - x0); + x = area.right; + } else if (r & R_LEFT) { + y = y0 + (y1 - y0) * (area.left - x0) / (x1 - x0); + x = area.left; + } + + if (r === r0) { + x0 = x; + y0 = y; + r0 = region(x0, y0, area); + } else { + x1 = x; + y1 = y; + r1 = region(x1, y1, area); + } + } + + return { + x0: x0, + x1: x1, + y0: y0, + y1: y1 + }; +} + +function compute$1(range, config) { + var anchor = config.anchor; + var segment = range; + var x, y; + + if (config.clamp) { + segment = clipped(segment, config.area); + } + + if (anchor === 'start') { + x = segment.x0; + y = segment.y0; + } else if (anchor === 'end') { + x = segment.x1; + y = segment.y1; + } else { + x = (segment.x0 + segment.x1) / 2; + y = (segment.y0 + segment.y1) / 2; + } + + return aligned(x, y, range.vx, range.vy, config.align); +} + +var positioners = { + arc: function(el, config) { + var angle = (el.startAngle + el.endAngle) / 2; + var vx = Math.cos(angle); + var vy = Math.sin(angle); + var r0 = el.innerRadius; + var r1 = el.outerRadius; + + return compute$1({ + x0: el.x + vx * r0, + y0: el.y + vy * r0, + x1: el.x + vx * r1, + y1: el.y + vy * r1, + vx: vx, + vy: vy + }, config); + }, + + point: function(el, config) { + var v = orient(el, config.origin); + var rx = v.x * el.options.radius; + var ry = v.y * el.options.radius; + + return compute$1({ + x0: el.x - rx, + y0: el.y - ry, + x1: el.x + rx, + y1: el.y + ry, + vx: v.x, + vy: v.y + }, config); + }, + + bar: function(el, config) { + var v = orient(el, config.origin); + var x = el.x; + var y = el.y; + var sx = 0; + var sy = 0; + + if (el.horizontal) { + x = Math.min(el.x, el.base); + sx = Math.abs(el.base - el.x); + } else { + y = Math.min(el.y, el.base); + sy = Math.abs(el.base - el.y); + } + + return compute$1({ + x0: x, + y0: y + sy, + x1: x + sx, + y1: y, + vx: v.x, + vy: v.y + }, config); + }, + + fallback: function(el, config) { + var v = orient(el, config.origin); + + return compute$1({ + x0: el.x, + y0: el.y, + x1: el.x + (el.width || 0), + y1: el.y + (el.height || 0), + vx: v.x, + vy: v.y + }, config); + } +}; + +var rasterize = utils.rasterize; + +function boundingRects(model) { + var borderWidth = model.borderWidth || 0; + var padding = model.padding; + var th = model.size.height; + var tw = model.size.width; + var tx = -tw / 2; + var ty = -th / 2; + + return { + frame: { + x: tx - padding.left - borderWidth, + y: ty - padding.top - borderWidth, + w: tw + padding.width + borderWidth * 2, + h: th + padding.height + borderWidth * 2 + }, + text: { + x: tx, + y: ty, + w: tw, + h: th + } + }; +} + +function getScaleOrigin(el, context) { + var scale = context.chart.getDatasetMeta(context.datasetIndex).vScale; + + if (!scale) { + return null; + } + + if (scale.xCenter !== undefined && scale.yCenter !== undefined) { + return {x: scale.xCenter, y: scale.yCenter}; + } + + var pixel = scale.getBasePixel(); + return el.horizontal ? + {x: pixel, y: null} : + {x: null, y: pixel}; +} + +function getPositioner(el) { + if (el instanceof chart_js.ArcElement) { + return positioners.arc; + } + if (el instanceof chart_js.PointElement) { + return positioners.point; + } + if (el instanceof chart_js.BarElement) { + return positioners.bar; + } + return positioners.fallback; +} + +function drawRoundedRect(ctx, x, y, w, h, radius) { + var HALF_PI = Math.PI / 2; + + if (radius) { + var r = Math.min(radius, h / 2, w / 2); + var left = x + r; + var top = y + r; + var right = x + w - r; + var bottom = y + h - r; + + ctx.moveTo(x, top); + if (left < right && top < bottom) { + ctx.arc(left, top, r, -Math.PI, -HALF_PI); + ctx.arc(right, top, r, -HALF_PI, 0); + ctx.arc(right, bottom, r, 0, HALF_PI); + ctx.arc(left, bottom, r, HALF_PI, Math.PI); + } else if (left < right) { + ctx.moveTo(left, y); + ctx.arc(right, top, r, -HALF_PI, HALF_PI); + ctx.arc(left, top, r, HALF_PI, Math.PI + HALF_PI); + } else if (top < bottom) { + ctx.arc(left, top, r, -Math.PI, 0); + ctx.arc(left, bottom, r, 0, Math.PI); + } else { + ctx.arc(left, top, r, -Math.PI, Math.PI); + } + ctx.closePath(); + ctx.moveTo(x, y); + } else { + ctx.rect(x, y, w, h); + } +} + +function drawFrame(ctx, rect, model) { + var bgColor = model.backgroundColor; + var borderColor = model.borderColor; + var borderWidth = model.borderWidth; + + if (!bgColor && (!borderColor || !borderWidth)) { + return; + } + + ctx.beginPath(); + + drawRoundedRect( + ctx, + rasterize(rect.x) + borderWidth / 2, + rasterize(rect.y) + borderWidth / 2, + rasterize(rect.w) - borderWidth, + rasterize(rect.h) - borderWidth, + model.borderRadius); + + ctx.closePath(); + + if (bgColor) { + ctx.fillStyle = bgColor; + ctx.fill(); + } + + if (borderColor && borderWidth) { + ctx.strokeStyle = borderColor; + ctx.lineWidth = borderWidth; + ctx.lineJoin = 'miter'; + ctx.stroke(); + } +} + +function textGeometry(rect, align, font) { + var h = font.lineHeight; + var w = rect.w; + var x = rect.x; + var y = rect.y + h / 2; + + if (align === 'center') { + x += w / 2; + } else if (align === 'end' || align === 'right') { + x += w; + } + + return { + h: h, + w: w, + x: x, + y: y + }; +} + +function drawTextLine(ctx, text, cfg) { + var shadow = ctx.shadowBlur; + var stroked = cfg.stroked; + var x = rasterize(cfg.x); + var y = rasterize(cfg.y); + var w = rasterize(cfg.w); + + if (stroked) { + ctx.strokeText(text, x, y, w); + } + + if (cfg.filled) { + if (shadow && stroked) { + // Prevent drawing shadow on both the text stroke and fill, so + // if the text is stroked, remove the shadow for the text fill. + ctx.shadowBlur = 0; + } + + ctx.fillText(text, x, y, w); + + if (shadow && stroked) { + ctx.shadowBlur = shadow; + } + } +} + +function drawText(ctx, lines, rect, model) { + var align = model.textAlign; + var color = model.color; + var filled = !!color; + var font = model.font; + var ilen = lines.length; + var strokeColor = model.textStrokeColor; + var strokeWidth = model.textStrokeWidth; + var stroked = strokeColor && strokeWidth; + var i; + + if (!ilen || (!filled && !stroked)) { + return; + } + + // Adjust coordinates based on text alignment and line height + rect = textGeometry(rect, align, font); + + ctx.font = font.string; + ctx.textAlign = align; + ctx.textBaseline = 'middle'; + ctx.shadowBlur = model.textShadowBlur; + ctx.shadowColor = model.textShadowColor; + + if (filled) { + ctx.fillStyle = color; + } + if (stroked) { + ctx.lineJoin = 'round'; + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = strokeColor; + } + + for (i = 0, ilen = lines.length; i < ilen; ++i) { + drawTextLine(ctx, lines[i], { + stroked: stroked, + filled: filled, + w: rect.w, + x: rect.x, + y: rect.y + rect.h * i + }); + } +} + +var Label = function(config, ctx, el, index) { + var me = this; + + me._config = config; + me._index = index; + me._model = null; + me._rects = null; + me._ctx = ctx; + me._el = el; +}; + +helpers.merge(Label.prototype, { + /** + * @private + */ + _modelize: function(display, lines, config, context) { + var me = this; + var index = me._index; + var font = helpers.toFont(helpers.resolve([config.font, {}], context, index)); + var color = helpers.resolve([config.color, chart_js.defaults.color], context, index); + + return { + align: helpers.resolve([config.align, 'center'], context, index), + anchor: helpers.resolve([config.anchor, 'center'], context, index), + area: context.chart.chartArea, + backgroundColor: helpers.resolve([config.backgroundColor, null], context, index), + borderColor: helpers.resolve([config.borderColor, null], context, index), + borderRadius: helpers.resolve([config.borderRadius, 0], context, index), + borderWidth: helpers.resolve([config.borderWidth, 0], context, index), + clamp: helpers.resolve([config.clamp, false], context, index), + clip: helpers.resolve([config.clip, false], context, index), + color: color, + display: display, + font: font, + lines: lines, + offset: helpers.resolve([config.offset, 4], context, index), + opacity: helpers.resolve([config.opacity, 1], context, index), + origin: getScaleOrigin(me._el, context), + padding: helpers.toPadding(helpers.resolve([config.padding, 4], context, index)), + positioner: getPositioner(me._el), + rotation: helpers.resolve([config.rotation, 0], context, index) * (Math.PI / 180), + size: utils.textSize(me._ctx, lines, font), + textAlign: helpers.resolve([config.textAlign, 'start'], context, index), + textShadowBlur: helpers.resolve([config.textShadowBlur, 0], context, index), + textShadowColor: helpers.resolve([config.textShadowColor, color], context, index), + textStrokeColor: helpers.resolve([config.textStrokeColor, color], context, index), + textStrokeWidth: helpers.resolve([config.textStrokeWidth, 0], context, index) + }; + }, + + update: function(context) { + var me = this; + var model = null; + var rects = null; + var index = me._index; + var config = me._config; + var value, label, lines; + + // We first resolve the display option (separately) to avoid computing + // other options in case the label is hidden (i.e. display: false). + var display = helpers.resolve([config.display, true], context, index); + + if (display) { + value = context.dataset.data[index]; + label = helpers.valueOrDefault(helpers.callback(config.formatter, [value, context]), value); + lines = helpers.isNullOrUndef(label) ? [] : utils.toTextLines(label); + + if (lines.length) { + model = me._modelize(display, lines, config, context); + rects = boundingRects(model); + } + } + + me._model = model; + me._rects = rects; + }, + + geometry: function() { + return this._rects ? this._rects.frame : {}; + }, + + rotation: function() { + return this._model ? this._model.rotation : 0; + }, + + visible: function() { + return this._model && this._model.opacity; + }, + + model: function() { + return this._model; + }, + + draw: function(chart, center) { + var me = this; + var ctx = chart.ctx; + var model = me._model; + var rects = me._rects; + var area; + + if (!this.visible()) { + return; + } + + ctx.save(); + + if (model.clip) { + area = model.area; + ctx.beginPath(); + ctx.rect( + area.left, + area.top, + area.right - area.left, + area.bottom - area.top); + ctx.clip(); + } + + ctx.globalAlpha = utils.bound(0, model.opacity, 1); + ctx.translate(rasterize(center.x), rasterize(center.y)); + ctx.rotate(model.rotation); + + drawFrame(ctx, rects.frame, model); + drawText(ctx, model.lines, rects.text, model); + + ctx.restore(); + } +}); + +var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; // eslint-disable-line es/no-number-minsafeinteger +var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; // eslint-disable-line es/no-number-maxsafeinteger + +function rotated(point, center, angle) { + var cos = Math.cos(angle); + var sin = Math.sin(angle); + var cx = center.x; + var cy = center.y; + + return { + x: cx + cos * (point.x - cx) - sin * (point.y - cy), + y: cy + sin * (point.x - cx) + cos * (point.y - cy) + }; +} + +function projected(points, axis) { + var min = MAX_INTEGER; + var max = MIN_INTEGER; + var origin = axis.origin; + var i, pt, vx, vy, dp; + + for (i = 0; i < points.length; ++i) { + pt = points[i]; + vx = pt.x - origin.x; + vy = pt.y - origin.y; + dp = axis.vx * vx + axis.vy * vy; + min = Math.min(min, dp); + max = Math.max(max, dp); + } + + return { + min: min, + max: max + }; +} + +function toAxis(p0, p1) { + var vx = p1.x - p0.x; + var vy = p1.y - p0.y; + var ln = Math.sqrt(vx * vx + vy * vy); + + return { + vx: (p1.x - p0.x) / ln, + vy: (p1.y - p0.y) / ln, + origin: p0, + ln: ln + }; +} + +var HitBox = function() { + this._rotation = 0; + this._rect = { + x: 0, + y: 0, + w: 0, + h: 0 + }; +}; + +helpers.merge(HitBox.prototype, { + center: function() { + var r = this._rect; + return { + x: r.x + r.w / 2, + y: r.y + r.h / 2 + }; + }, + + update: function(center, rect, rotation) { + this._rotation = rotation; + this._rect = { + x: rect.x + center.x, + y: rect.y + center.y, + w: rect.w, + h: rect.h + }; + }, + + contains: function(point) { + var me = this; + var margin = 1; + var rect = me._rect; + + point = rotated(point, me.center(), -me._rotation); + + return !(point.x < rect.x - margin + || point.y < rect.y - margin + || point.x > rect.x + rect.w + margin * 2 + || point.y > rect.y + rect.h + margin * 2); + }, + + // Separating Axis Theorem + // https://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169 + intersects: function(other) { + var r0 = this._points(); + var r1 = other._points(); + var axes = [ + toAxis(r0[0], r0[1]), + toAxis(r0[0], r0[3]) + ]; + var i, pr0, pr1; + + if (this._rotation !== other._rotation) { + // Only separate with r1 axis if the rotation is different, + // else it's enough to separate r0 and r1 with r0 axis only! + axes.push( + toAxis(r1[0], r1[1]), + toAxis(r1[0], r1[3]) + ); + } + + for (i = 0; i < axes.length; ++i) { + pr0 = projected(r0, axes[i]); + pr1 = projected(r1, axes[i]); + + if (pr0.max < pr1.min || pr1.max < pr0.min) { + return false; + } + } + + return true; + }, + + /** + * @private + */ + _points: function() { + var me = this; + var rect = me._rect; + var angle = me._rotation; + var center = me.center(); + + return [ + rotated({x: rect.x, y: rect.y}, center, angle), + rotated({x: rect.x + rect.w, y: rect.y}, center, angle), + rotated({x: rect.x + rect.w, y: rect.y + rect.h}, center, angle), + rotated({x: rect.x, y: rect.y + rect.h}, center, angle) + ]; + } +}); + +function coordinates(el, model, geometry) { + var point = model.positioner(el, model); + var vx = point.vx; + var vy = point.vy; + + if (!vx && !vy) { + // if aligned center, we don't want to offset the center point + return {x: point.x, y: point.y}; + } + + var w = geometry.w; + var h = geometry.h; + + // take in account the label rotation + var rotation = model.rotation; + var dx = Math.abs(w / 2 * Math.cos(rotation)) + Math.abs(h / 2 * Math.sin(rotation)); + var dy = Math.abs(w / 2 * Math.sin(rotation)) + Math.abs(h / 2 * Math.cos(rotation)); + + // scale the unit vector (vx, vy) to get at least dx or dy equal to + // w or h respectively (else we would calculate the distance to the + // ellipse inscribed in the bounding rect) + var vs = 1 / Math.max(Math.abs(vx), Math.abs(vy)); + dx *= vx * vs; + dy *= vy * vs; + + // finally, include the explicit offset + dx += model.offset * vx; + dy += model.offset * vy; + + return { + x: point.x + dx, + y: point.y + dy + }; +} + +function collide(labels, collider) { + var i, j, s0, s1; + + // IMPORTANT Iterate in the reverse order since items at the end of the + // list have an higher weight/priority and thus should be less impacted + // by the overlapping strategy. + + for (i = labels.length - 1; i >= 0; --i) { + s0 = labels[i].$layout; + + for (j = i - 1; j >= 0 && s0._visible; --j) { + s1 = labels[j].$layout; + + if (s1._visible && s0._box.intersects(s1._box)) { + collider(s0, s1); + } + } + } + + return labels; +} + +function compute(labels) { + var i, ilen, label, state, geometry, center, proxy; + + // Initialize labels for overlap detection + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + state = label.$layout; + + if (state._visible) { + // Chart.js 3 removed el._model in favor of getProps(), making harder to + // abstract reading values in positioners. Also, using string arrays to + // read values (i.e. var {a,b,c} = el.getProps(["a","b","c"])) would make + // positioners inefficient in the normal case (i.e. not the final values) + // and the code a bit ugly, so let's use a Proxy instead. + proxy = new Proxy(label._el, {get: (el, p) => el.getProps([p], true)[p]}); + + geometry = label.geometry(); + center = coordinates(proxy, label.model(), geometry); + state._box.update(center, geometry, label.rotation()); + } + } + + // Auto hide overlapping labels + return collide(labels, function(s0, s1) { + var h0 = s0._hidable; + var h1 = s1._hidable; + + if ((h0 && h1) || h1) { + s1._visible = false; + } else if (h0) { + s0._visible = false; + } + }); +} + +var layout = { + prepare: function(datasets) { + var labels = []; + var i, j, ilen, jlen, label; + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + for (j = 0, jlen = datasets[i].length; j < jlen; ++j) { + label = datasets[i][j]; + labels.push(label); + label.$layout = { + _box: new HitBox(), + _hidable: false, + _visible: true, + _set: i, + _idx: label._index + }; + } + } + + // TODO New `z` option: labels with a higher z-index are drawn + // of top of the ones with a lower index. Lowest z-index labels + // are also discarded first when hiding overlapping labels. + labels.sort(function(a, b) { + var sa = a.$layout; + var sb = b.$layout; + + return sa._idx === sb._idx + ? sb._set - sa._set + : sb._idx - sa._idx; + }); + + this.update(labels); + + return labels; + }, + + update: function(labels) { + var dirty = false; + var i, ilen, label, model, state; + + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + model = label.model(); + state = label.$layout; + state._hidable = model && model.display === 'auto'; + state._visible = label.visible(); + dirty |= state._hidable; + } + + if (dirty) { + compute(labels); + } + }, + + lookup: function(labels, point) { + var i, state; + + // IMPORTANT Iterate in the reverse order since items at the end of + // the list have an higher z-index, thus should be picked first. + + for (i = labels.length - 1; i >= 0; --i) { + state = labels[i].$layout; + + if (state && state._visible && state._box.contains(point)) { + return labels[i]; + } + } + + return null; + }, + + draw: function(chart, labels) { + var i, ilen, label, state, geometry, center; + + for (i = 0, ilen = labels.length; i < ilen; ++i) { + label = labels[i]; + state = label.$layout; + + if (state._visible) { + geometry = label.geometry(); + center = coordinates(label._el, label.model(), geometry); + state._box.update(center, geometry, label.rotation()); + label.draw(chart, center); + } + } + } +}; + +var formatter = function(value) { + if (helpers.isNullOrUndef(value)) { + return null; + } + + var label = value; + var keys, klen, k; + if (helpers.isObject(value)) { + if (!helpers.isNullOrUndef(value.label)) { + label = value.label; + } else if (!helpers.isNullOrUndef(value.r)) { + label = value.r; + } else { + label = ''; + keys = Object.keys(value); + for (k = 0, klen = keys.length; k < klen; ++k) { + label += (k !== 0 ? ', ' : '') + keys[k] + ': ' + value[keys[k]]; + } + } + } + + return '' + label; +}; + +/** + * IMPORTANT: make sure to also update tests and TypeScript definition + * files (`/test/specs/defaults.spec.js` and `/types/options.d.ts`) + */ + +var defaults = { + align: 'center', + anchor: 'center', + backgroundColor: null, + borderColor: null, + borderRadius: 0, + borderWidth: 0, + clamp: false, + clip: false, + color: undefined, + display: true, + font: { + family: undefined, + lineHeight: 1.2, + size: undefined, + style: undefined, + weight: null + }, + formatter: formatter, + labels: undefined, + listeners: {}, + offset: 4, + opacity: 1, + padding: { + top: 4, + right: 4, + bottom: 4, + left: 4 + }, + rotation: 0, + textAlign: 'start', + textStrokeColor: undefined, + textStrokeWidth: 0, + textShadowBlur: 0, + textShadowColor: undefined +}; + +/** + * @see https://github.com/chartjs/Chart.js/issues/4176 + */ + +var EXPANDO_KEY = '$datalabels'; +var DEFAULT_KEY = '$default'; + +function configure(dataset, options) { + var override = dataset.datalabels; + var listeners = {}; + var configs = []; + var labels, keys; + + if (override === false) { + return null; + } + if (override === true) { + override = {}; + } + + options = helpers.merge({}, [options, override]); + labels = options.labels || {}; + keys = Object.keys(labels); + delete options.labels; + + if (keys.length) { + keys.forEach(function(key) { + if (labels[key]) { + configs.push(helpers.merge({}, [ + options, + labels[key], + {_key: key} + ])); + } + }); + } else { + // Default label if no "named" label defined. + configs.push(options); + } + + // listeners: {: {: }} + listeners = configs.reduce(function(target, config) { + helpers.each(config.listeners || {}, function(fn, event) { + target[event] = target[event] || {}; + target[event][config._key || DEFAULT_KEY] = fn; + }); + + delete config.listeners; + return target; + }, {}); + + return { + labels: configs, + listeners: listeners + }; +} + +function dispatchEvent(chart, listeners, label, event) { + if (!listeners) { + return; + } + + var context = label.$context; + var groups = label.$groups; + var callback; + + if (!listeners[groups._set]) { + return; + } + + callback = listeners[groups._set][groups._key]; + if (!callback) { + return; + } + + if (helpers.callback(callback, [context, event]) === true) { + // Users are allowed to tweak the given context by injecting values that can be + // used in scriptable options to display labels differently based on the current + // event (e.g. highlight an hovered label). That's why we update the label with + // the output context and schedule a new chart render by setting it dirty. + chart[EXPANDO_KEY]._dirty = true; + label.update(context); + } +} + +function dispatchMoveEvents(chart, listeners, previous, label, event) { + var enter, leave; + + if (!previous && !label) { + return; + } + + if (!previous) { + enter = true; + } else if (!label) { + leave = true; + } else if (previous !== label) { + leave = enter = true; + } + + if (leave) { + dispatchEvent(chart, listeners.leave, previous, event); + } + if (enter) { + dispatchEvent(chart, listeners.enter, label, event); + } +} + +function handleMoveEvents(chart, event) { + var expando = chart[EXPANDO_KEY]; + var listeners = expando._listeners; + var previous, label; + + if (!listeners.enter && !listeners.leave) { + return; + } + + if (event.type === 'mousemove') { + label = layout.lookup(expando._labels, event); + } else if (event.type !== 'mouseout') { + return; + } + + previous = expando._hovered; + expando._hovered = label; + dispatchMoveEvents(chart, listeners, previous, label, event); +} + +function handleClickEvents(chart, event) { + var expando = chart[EXPANDO_KEY]; + var handlers = expando._listeners.click; + var label = handlers && layout.lookup(expando._labels, event); + if (label) { + dispatchEvent(chart, handlers, label, event); + } +} + +var plugin = { + id: 'datalabels', + + defaults: defaults, + + beforeInit: function(chart) { + chart[EXPANDO_KEY] = { + _actives: [] + }; + }, + + beforeUpdate: function(chart) { + var expando = chart[EXPANDO_KEY]; + expando._listened = false; + expando._listeners = {}; // {: {: {: }}} + expando._datasets = []; // per dataset labels: [Label[]] + expando._labels = []; // layouted labels: Label[] + }, + + afterDatasetUpdate: function(chart, args, options) { + var datasetIndex = args.index; + var expando = chart[EXPANDO_KEY]; + var labels = expando._datasets[datasetIndex] = []; + var visible = chart.isDatasetVisible(datasetIndex); + var dataset = chart.data.datasets[datasetIndex]; + var config = configure(dataset, options); + var elements = args.meta.data || []; + var ctx = chart.ctx; + var i, j, ilen, jlen, cfg, key, el, label; + + ctx.save(); + + for (i = 0, ilen = elements.length; i < ilen; ++i) { + el = elements[i]; + el[EXPANDO_KEY] = []; + + if (visible && el && chart.getDataVisibility(i) && !el.skip) { + for (j = 0, jlen = config.labels.length; j < jlen; ++j) { + cfg = config.labels[j]; + key = cfg._key; + + label = new Label(cfg, ctx, el, i); + label.$groups = { + _set: datasetIndex, + _key: key || DEFAULT_KEY + }; + label.$context = { + active: false, + chart: chart, + dataIndex: i, + dataset: dataset, + datasetIndex: datasetIndex + }; + + label.update(label.$context); + el[EXPANDO_KEY].push(label); + labels.push(label); + } + } + } + + ctx.restore(); + + // Store listeners at the chart level and per event type to optimize + // cases where no listeners are registered for a specific event. + helpers.merge(expando._listeners, config.listeners, { + merger: function(event, target, source) { + target[event] = target[event] || {}; + target[event][args.index] = source[event]; + expando._listened = true; + } + }); + }, + + afterUpdate: function(chart) { + chart[EXPANDO_KEY]._labels = layout.prepare(chart[EXPANDO_KEY]._datasets); + }, + + // Draw labels on top of all dataset elements + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/29 + // https://github.com/chartjs/chartjs-plugin-datalabels/issues/32 + afterDatasetsDraw: function(chart) { + layout.draw(chart, chart[EXPANDO_KEY]._labels); + }, + + beforeEvent: function(chart, args) { + // If there is no listener registered for this chart, `listened` will be false, + // meaning we can immediately ignore the incoming event and avoid useless extra + // computation for users who don't implement label interactions. + if (chart[EXPANDO_KEY]._listened) { + var event = args.event; + switch (event.type) { + case 'mousemove': + case 'mouseout': + handleMoveEvents(chart, event); + break; + case 'click': + handleClickEvents(chart, event); + break; + } + } + }, + + afterEvent: function(chart) { + var expando = chart[EXPANDO_KEY]; + var previous = expando._actives; + var actives = expando._actives = chart.getActiveElements(); + var updates = utils.arrayDiff(previous, actives); + var i, ilen, j, jlen, update, label, labels; + + for (i = 0, ilen = updates.length; i < ilen; ++i) { + update = updates[i]; + if (update[1]) { + labels = update[0].element[EXPANDO_KEY] || []; + for (j = 0, jlen = labels.length; j < jlen; ++j) { + label = labels[j]; + label.$context.active = (update[1] === 1); + label.update(label.$context); + } + } + } + + if (expando._dirty || updates.length) { + layout.update(expando._labels); + chart.render(); + } + + delete expando._dirty; + } +}; + +return plugin; + +})); diff --git a/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.min.js b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.min.js new file mode 100644 index 0000000..e84e6ff --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/chartjs-plugin-datalabels.min.js @@ -0,0 +1,7 @@ +/*! + * chartjs-plugin-datalabels v2.2.0 + * https://chartjs-plugin-datalabels.netlify.app + * (c) 2017-2022 chartjs-plugin-datalabels contributors + * Released under the MIT license + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js/helpers"),require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js/helpers","chart.js"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).ChartDataLabels=e(t.Chart.helpers,t.Chart)}(this,(function(t,e){"use strict";var r=function(){if("undefined"!=typeof window){if(window.devicePixelRatio)return window.devicePixelRatio;var t=window.screen;if(t)return(t.deviceXDPI||1)/(t.logicalXDPI||1)}return 1}(),a=function(e){var r,a=[];for(e=[].concat(e);e.length;)"string"==typeof(r=e.pop())?a.unshift.apply(a,r.split("\n")):Array.isArray(r)?e.push.apply(e,r):t.isNullOrUndef(e)||a.unshift(""+r);return a},o=function(t,e,r){var a,o=[].concat(e),n=o.length,i=t.font,l=0;for(t.font=r.string,a=0;ar.right&&(a|=2),er.bottom&&(a|=4),a}function u(t,e){var r,a,o=e.anchor,n=t;return e.clamp&&(n=function(t,e){for(var r,a,o,n=t.x0,i=t.y0,l=t.x1,u=t.y1,d=s(n,i,e),c=s(l,u,e);d|c&&!(d&c);)8&(r=d||c)?(a=n+(l-n)*(e.top-i)/(u-i),o=e.top):4&r?(a=n+(l-n)*(e.bottom-i)/(u-i),o=e.bottom):2&r?(o=i+(u-i)*(e.right-n)/(l-n),a=e.right):1&r&&(o=i+(u-i)*(e.left-n)/(l-n),a=e.left),r===d?d=s(n=a,i=o,e):c=s(l=a,u=o,e);return{x0:n,x1:l,y0:i,y1:u}}(n,e.area)),"start"===o?(r=n.x0,a=n.y0):"end"===o?(r=n.x1,a=n.y1):(r=(n.x0+n.x1)/2,a=(n.y0+n.y1)/2),function(t,e,r,a,o){switch(o){case"center":r=a=0;break;case"bottom":r=0,a=1;break;case"right":r=1,a=0;break;case"left":r=-1,a=0;break;case"top":r=0,a=-1;break;case"start":r=-r,a=-a;break;case"end":break;default:o*=Math.PI/180,r=Math.cos(o),a=Math.sin(o)}return{x:t,y:e,vx:r,vy:a}}(r,a,t.vx,t.vy,e.align)}var d=function(t,e){var r=(t.startAngle+t.endAngle)/2,a=Math.cos(r),o=Math.sin(r),n=t.innerRadius,i=t.outerRadius;return u({x0:t.x+a*n,y0:t.y+o*n,x1:t.x+a*i,y1:t.y+o*i,vx:a,vy:o},e)},c=function(t,e){var r=l(t,e.origin),a=r.x*t.options.radius,o=r.y*t.options.radius;return u({x0:t.x-a,y0:t.y-o,x1:t.x+a,y1:t.y+o,vx:r.x,vy:r.y},e)},h=function(t,e){var r=l(t,e.origin),a=t.x,o=t.y,n=0,i=0;return t.horizontal?(a=Math.min(t.x,t.base),n=Math.abs(t.base-t.x)):(o=Math.min(t.y,t.base),i=Math.abs(t.base-t.y)),u({x0:a,y0:o+i,x1:a+n,y1:o,vx:r.x,vy:r.y},e)},f=function(t,e){var r=l(t,e.origin);return u({x0:t.x,y0:t.y,x1:t.x+(t.width||0),y1:t.y+(t.height||0),vx:r.x,vy:r.y},e)},x=function(t){return Math.round(t*r)/r};function y(t,e){var r=e.chart.getDatasetMeta(e.datasetIndex).vScale;if(!r)return null;if(void 0!==r.xCenter&&void 0!==r.yCenter)return{x:r.xCenter,y:r.yCenter};var a=r.getBasePixel();return t.horizontal?{x:a,y:null}:{x:null,y:a}}function v(t,e,r){var a=r.backgroundColor,o=r.borderColor,n=r.borderWidth;(a||o&&n)&&(t.beginPath(),function(t,e,r,a,o,n){var i=Math.PI/2;if(n){var l=Math.min(n,o/2,a/2),s=e+l,u=r+l,d=e+a-l,c=r+o-l;t.moveTo(e,u),sr.x+r.w+2||t.y>r.y+r.h+2)},intersects:function(t){var e,r,a,o=this._points(),n=t._points(),i=[M(o[0],o[1]),M(o[0],o[3])];for(this._rotation!==t._rotation&&i.push(M(n[0],n[1]),M(n[0],n[3])),e=0;et.getProps([e],!0)[e]}),n=a.geometry(),i=$(l,a.model(),n),o._box.update(i,n,a.rotation()));(function(t,e){var r,a,o,n;for(r=t.length-1;r>=0;--r)for(o=t[r].$layout,a=r-1;a>=0&&o._visible;--a)(n=t[a].$layout)._visible&&o._box.intersects(n._box)&&e(o,n)})(t,(function(t,e){var r=t._hidable,a=e._hidable;r&&a||a?e._visible=!1:r&&(t._visible=!1)}))}(t)},lookup:function(t,e){var r,a;for(r=t.length-1;r>=0;--r)if((a=t[r].$layout)&&a._visible&&a._box.contains(e))return t[r];return null},draw:function(t,e){var r,a,o,n,i,l;for(r=0,a=e.length;r { + document.getElementById(elementId).click(); +} + +window.DownloadImage = (url, fileName) => { + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +window.DownloadImageBase64 = (base64Data, fileName) => { + // Chuyển đổi Base64 thành Blob + const byteCharacters = atob(base64Data.split(',')[1]); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + const blob = new Blob([byteArray], { type: 'image/png' }); // Thay đổi type nếu cần + + // Tạo Object URL từ Blob + const url = URL.createObjectURL(blob); + + // Tạo thẻ `` để tải về + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Giải phóng bộ nhớ + URL.revokeObjectURL(url); +} + +window.setCssVariable = (variable, value) => { + document.documentElement.style.setProperty(variable, value); + localStorage.setItem(variable, value); +}; + +window.getCssVariable = (variable) => { + return localStorage.getItem(variable); +}; + +window.DOMCssLoaded = () => { + const width = localStorage.getItem('--edge-stroke-width'); + if (width) { + document.documentElement.style.setProperty('--edge-stroke-width', width); + } + else { + document.documentElement.style.setProperty('--edge-stroke-width', "0.15px"); + } + + const directionwidth = localStorage.getItem('--edge-direction-stroke-width'); + if (directionwidth) { + document.documentElement.style.setProperty('--edge-direction-stroke-width', directionwidth); + } + else { + document.documentElement.style.setProperty('--edge-direction-stroke-width', "0.3px"); + } + + const radius = localStorage.getItem('--node-r'); + if (radius) { + document.documentElement.style.setProperty('--node-r', radius); + } + else { + document.documentElement.style.setProperty('--node-r', "0.1px"); + } + + const originwidth = localStorage.getItem('--origin-vector-stroke-width'); + if (originwidth) { + document.documentElement.style.setProperty('--origin-vector-stroke-width', originwidth); + } + else { + document.documentElement.style.setProperty('--origin-vector-stroke-width', "0.35px"); + } +}; + +window.UpdateViewContainerRect = async (dotnetRef, divElement, funcName) => { + var rect = divElement.getBoundingClientRect(); + await dotnetRef.invokeMethodAsync(funcName, rect.x, rect.y, rect.width, rect.height, rect.top, rect.right, rect.bottom, rect.left); +}; + +window.ResizeObserverRegister = (dotnetRef, divElement, funcName) => { + const resizeObserver = new ResizeObserver(async (entries) => { + await window.UpdateViewContainerRect(dotnetRef, divElement, funcName); + }); + + resizeObserver.observe(divElement); +}; + +window.AddEventListener = (dotnetRef, element, eventName, funcName, stopPropagation = true) => { + element.addEventListener(eventName, async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName); + }) +}; + +window.AddMouseMoveEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mousemove", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.clientX, ev.clientY, ev.buttons, ev.ctrlKey, ev.movementX, ev.movementY); + }) +}; + +window.AddKeyUpEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("keyup", async (ev) => { + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.code, ev.key, ev.altKey, ev.ctrlKey, ev.shiftKey); + }) +}; + +window.AddMouseWheelEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mousewheel", async (ev) => { + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.deltaY, ev.offsetX, ev.offsetY); + }, { passive: true }) +}; + +window.AddMouseUpEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mouseup", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.button, ev.altKey, ev.ctrlKey, ev.shiftKey); + }) +}; + +window.AddMouseDownEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mousedown", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.button, ev.altKey, ev.ctrlKey, ev.shiftKey); + }) +}; + +window.SetMapSvgConfig = (element, width, height, originX, originY) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "svg") { + element.setAttribute("width", width ); + element.setAttribute("height", height ); + element.setAttribute("viewBox", `${originX} ${originY} ${width} ${height}`); + } +}; + +window.SetMapSvgRect = (element, width, height) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "svg") { + element.setAttribute("width", width); + element.setAttribute("height", height); + } +}; + +window.SetImageAttribute = (element, width, height, originX, originY, href) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "image") { + element.setAttribute("width", width ); + element.setAttribute("height", height ); + element.setAttribute("x", originX); + element.setAttribute("y", originY); + element.setAttribute("href", href); + } +}; + +window.SetMapMovement = (element, top, left) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "div") { + element.setAttribute("style", `top: ${top}px; left: ${left}px;`); + } +}; + +window.SetMapScale = (element, scale) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "div") { + element.setAttribute("style", `transform: scale(${scale });`); + } +}; + +window.SetNodePosition = (circleRef, textRef, x, y) => { + if (circleRef && circleRef.setAttribute) { + circleRef.setAttribute("cx", x); + circleRef.setAttribute("cy", y); + } + if (textRef && textRef.setAttribute) { + const radius = parseFloat(localStorage.getItem('--node-r')); + if (!radius) { + radius = 0.1; + } + textRef.setAttribute("x", x); + textRef.setAttribute("y", - y - radius - 0.08); + } +}; + +window.AddSelected = (element) => { + element.classList.add("active"); +}; + +window.RemoveSelected = (element) => { + element.classList.remove("active"); +}; + +window.ElementSetAttribute = (element, attr, value) => { + if (element && element.setAttribute) element.setAttribute(attr, value); +} + +window.AddTouchMoveEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + let lastX = 0, lastY = 0; + element.addEventListener("touchmove", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + + const touch = ev.touches[0]; + const movementX = touch.clientX - lastX; + const movementY = touch.clientY - lastY; + + lastX = touch.clientX; + lastY = touch.clientY; + await dotnetRef.invokeMethodAsync(funcName, touch.clientX, touch.clientY, ev.touches.length, movementX, movementY); + }); + + element.addEventListener("touchstart", (ev) => { + const touch = ev.touches[0]; + lastX = touch.clientX; + lastY = touch.clientY; + }); +}; + +window.DownloadMap = async (filename, content) => { + const arrayBuffer = await content.arrayBuffer(); + const blob = new Blob([arrayBuffer]); + const url = URL.createObjectURL(blob); + + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = filename; + anchor.click(); + + URL.revokeObjectURL(url); +} + +window.SetElementPosition = (robotRef, nameRef, x, y, theta, originX, originY) => { + if (robotRef && robotRef.setAttribute) { + robotRef.setAttribute("x", x + originX); + robotRef.setAttribute("y", y + originY); + robotRef.setAttribute("transform", `rotate(${theta} ${x} ${y})`); + } + if (nameRef && nameRef.setAttribute) { + nameRef.setAttribute("x", x); + nameRef.setAttribute("y", -y); + } +} + +window.ScrollToBottom = (objDiv) => { + if (objDiv.scrollTop != objDiv.scrollHeight) { + objDiv.scrollTop = objDiv.scrollHeight; + } +} \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/js/robonet.chart.js b/RobotNet.WebApp/wwwroot/js/robonet.chart.js new file mode 100644 index 0000000..d65899f --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/robonet.chart.js @@ -0,0 +1,176 @@ +if (!window.blazorChart) { + window.blazorChart = {}; +} + + +window.blazorChart = { + addDatasetData: (elementId, dataLabel, data) => { + let chart = window.blazorChart.get(elementId); + if (chart) { + const chartData = chart.data; + const chartDatasetData = data; + + if (!chartData.labels.includes(dataLabel)) + chartData.labels.push(dataLabel); + + const chartDatasets = chartData.datasets; + + if (chartDatasets.length > 0) { + let datasetIndex = chartDatasets.findIndex(dataset => dataset.label === chartDatasetData.datasetLabel); + if (datasetIndex > -1) { + chartDatasets[datasetIndex].data.push(chartDatasetData.data); + chart.update(); + } + } + } + }, + addDatasetsData: (elementId, dataLabel, data) => { + let chart = window.blazorChart.get(elementId); + if (chart && data) { + const chartData = chart.data; + + if (!chartData.labels.includes(dataLabel)) { + chartData.labels.push(dataLabel); + + if (chartData.datasets.length > 0 && chartData.datasets.length === data.length) { + data.forEach(chartDatasetData => { + let datasetIndex = chartData.datasets.findIndex(dataset => dataset.label === chartDatasetData.datasetLabel); + chartData.datasets[datasetIndex].data.push(chartDatasetData.data); + }); + chart.update(); + } + } + } + }, + addDataset: (elementId, newDataset) => { + let chart = window.blazorChart.get(elementId); + if (chart) { + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + create: (elementId, type, data, options) => { + let chartEl = document.getElementById(elementId); + let _plugins = []; + + _plugins.push(ChartDataLabels); + const config = { + type: type, + data: data, + options: options, + plugins: _plugins + }; + + if (type === 'line') { + const tooltipLine = { + id: 'tooltipLine', + beforeDraw: chart => { + if (chart.tooltip?._active && chart.tooltip?._active.length) { + const ctx = chart.ctx; + ctx.save(); + const activePoint = chart.tooltip._active[0]; + + ctx.beginPath(); + ctx.setLineDash([5, 5]); + ctx.moveTo(activePoint.element.x, chart.chartArea.top); + ctx.lineTo(activePoint.element.x, activePoint.element.y); + ctx.linewidth = 2; + ctx.strokeStyle = 'grey'; + ctx.stroke(); + ctx.restore(); + + ctx.beginPath(); + ctx.setLineDash([5, 5]); + ctx.moveTo(activePoint.element.x, activePoint.element.y); + ctx.lineTo(activePoint.element.x, chart.chartArea.bottom); + ctx.linewidth = 2; + ctx.strokeStyle = 'grey'; + ctx.stroke(); + ctx.restore(); + } + }, + }; + + config.plugins.push(tooltipLine); + } + + const chart = new Chart( + chartEl, + config + ); + }, + get: (elementId) => { + let chart; + Chart.helpers.each(Chart.instances, function (instance) { + if (instance.canvas.id === elementId) { + chart = instance; + } + }); + + return chart; + }, + initialize: (elementId, type, data, options) => { + let chart = window.blazorChart.get(elementId); + if (chart) return; + else + window.blazorChart.create(elementId, type, data, options); + }, + resize: (elementId, width, height) => { + let chart = window.blazorChart.get(elementId); + if (chart) { + chart.canvas.parentNode.style.height = height; + chart.canvas.parentNode.style.width = width; + } + }, + update: (elementId, data, options) => { + let chart = window.blazorChart.get(elementId); + if (chart) { + chart.data = data; + chart.options = options; + chart.update(); + } + else { + console.warn(`The chart is not initialized. Initialize it and then call update.`); + } + }, + updateData: (elementId, data) => { + let chart = window.blazorChart.get(elementId); + if (chart) { + window.blazorChart.mergeDatasets(chart.data.datasets, data.datasets); + window.blazorChart.mergeLabels(chart.data, data); + chart.update(); + } + }, + mergeDatasets(oldDatasets, newDatasets) { + for (let i = oldDatasets.length - 1; i >= 0; i--) { + let sameDatasetInNewConfig = newDatasets.find(newD => newD.label === oldDatasets[i].label); + if (sameDatasetInNewConfig === undefined) { + oldDatasets.splice(i, 1); + } + else { + oldDatasets[i].data = sameDatasetInNewConfig.data; + } + } + let currentIds = oldDatasets.map(dataset => dataset.label); + newDatasets.filter(newDataset => !currentIds.includes(newDataset.label)).forEach(newDataset => oldDatasets.push(newDataset)); + }, + mergeLabels(oldChartData, newChartData) { + const innerFunc = (oldLabels, newLabels) => { + if (newLabels == null || newLabels.length === 0) { + if (oldLabels) { + oldLabels.length = 0; + } + return oldLabels; + } + if (oldLabels == null) { + return newLabels; + } + oldLabels.length = 0; + for (var i = 0; i < newLabels.length; i++) { + oldLabels.push(newLabels[i]); + } + return oldLabels; + }; + oldChartData.labels = innerFunc(oldChartData.labels, newChartData.labels); + }, +} \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/js/robot-design.js b/RobotNet.WebApp/wwwroot/js/robot-design.js new file mode 100644 index 0000000..c4ce1cc --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/robot-design.js @@ -0,0 +1,119 @@ +window.ElementSetAttribute = (element, attr, value) => { + if (element && element.setAttribute) element.setAttribute(attr, value); +} + +window.SetImageAttribute = (element, width, height, originX, originY, href) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "image") { + element.setAttribute("width", width); + element.setAttribute("height", height); + element.setAttribute("x", originX); + element.setAttribute("y", originY); + element.setAttribute("href", href); + } +}; + +window.SetRobotPosition = (robotRef, nameRef, x, y, theta, originX, originY) => { + if (robotRef && robotRef.setAttribute) { + robotRef.setAttribute("x", x + originX); + robotRef.setAttribute("y", y + originY); + robotRef.setAttribute("transform", `rotate(${theta} ${x} ${y})`); + } + if (nameRef && nameRef.setAttribute) { + nameRef.setAttribute("x", x); + nameRef.setAttribute("y", -y); + } +} + +window.SetNodePosition = (circleRef, textRef, x, y) => { + if (circleRef && circleRef.setAttribute) { + circleRef.setAttribute("cx", x); + circleRef.setAttribute("cy", y); + } + if (textRef && textRef.setAttribute) { + const radius = parseFloat(localStorage.getItem('--node-r')); + if (!radius) { + radius = 0.1; + } + textRef.setAttribute("x", x); + textRef.setAttribute("y", - y - radius - 0.08); + } +}; + +window.UpdateViewContainerRect = async (dotnetRef, divElement, funcName) => { + var rect = divElement.getBoundingClientRect(); + await dotnetRef.invokeMethodAsync(funcName, rect.x, rect.y, rect.width, rect.height, rect.top, rect.right, rect.bottom, rect.left); +}; + +window.ResizeObserverRegister = (dotnetRef, divElement, funcName) => { + const resizeObserver = new ResizeObserver(async (entries) => { + await window.UpdateViewContainerRect(dotnetRef, divElement, funcName); + }); + + resizeObserver.observe(divElement); +}; + +window.AddEventListener = (dotnetRef, element, eventName, funcName, stopPropagation = true) => { + element.addEventListener(eventName, async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName); + }) +}; + +window.AddMouseMoveEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mousemove", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.clientX, ev.clientY, ev.buttons, ev.ctrlKey, ev.movementX, ev.movementY); + }) +}; + +window.AddMouseWheelEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + element.addEventListener("mousewheel", async (ev) => { + if (stopPropagation) ev.stopPropagation(); + await dotnetRef.invokeMethodAsync(funcName, ev.deltaY, ev.offsetX, ev.offsetY); + }, { passive: true }) +}; + +window.AddTouchMoveEventListener = (dotnetRef, element, funcName, stopPropagation = true) => { + let lastX = 0, lastY = 0; + element.addEventListener("touchmove", async (ev) => { + ev.preventDefault(); + if (stopPropagation) ev.stopPropagation(); + + const touch = ev.touches[0]; + const movementX = touch.clientX - lastX; + const movementY = touch.clientY - lastY; + + lastX = touch.clientX; + lastY = touch.clientY; + await dotnetRef.invokeMethodAsync(funcName, touch.clientX, touch.clientY, ev.touches.length, movementX, movementY); + }); + + element.addEventListener("touchstart", (ev) => { + const touch = ev.touches[0]; + lastX = touch.clientX; + lastY = touch.clientY; + }); +}; + +window.SetMapSvgConfig = (element, width, height, originX, originY) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "svg") { + element.setAttribute("width", width); + element.setAttribute("height", height); + element.setAttribute("viewBox", `${originX} ${originY} ${width} ${height}`); + } +}; + +window.SetMapMovement = (element, top, left) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "div") { + element.setAttribute("style", `top: ${top}px; left: ${left}px;`); + } +}; + +window.SetMapSvgRect = (element, width, height) => { + if (element && element.nodeName && element.nodeName.toLowerCase() === "svg") { + element.setAttribute("width", width); + element.setAttribute("height", height); + } +}; \ No newline at end of file diff --git a/RobotNet.WebApp/wwwroot/js/script.js b/RobotNet.WebApp/wwwroot/js/script.js new file mode 100644 index 0000000..76b408a --- /dev/null +++ b/RobotNet.WebApp/wwwroot/js/script.js @@ -0,0 +1,76 @@ +window.monaco = monaco; + +window.MonacoRuntime = { + OnCodeEditorDidChangeModelContent: (codeEditor, dotnetRef, method) => { + codeEditor.onDidChangeModelContent(function (e) { + dotnetRef.invokeMethodAsync(method, e); + }); + }, + CSharpLanguageRegisterCompletionItemProvider: (dotnetRef, getCompletionMethod) => { + return monaco.languages.registerCompletionItemProvider("csharp", { + triggerCharacters: ["."], + //resolveCompletionItem: (model, position, item) => { + // return this.resolveCompletionItem(item, dotnetHelper) + //}, + provideCompletionItems: async (model, position, context, token) => { + return await dotnetRef.invokeMethodAsync(getCompletionMethod, position.lineNumber, position.column, context.triggerKind, context.triggerCharacter); + } + }); + }, + CSharpLanguageRegisterSignatureHelpProvider: (dotnetRef, getSignatureHelpMethod) => { + return monaco.languages.registerSignatureHelpProvider("csharp", { + signatureHelpTriggerCharacters: ['('], + provideSignatureHelp: async (model, position, token, context) => { + let signature = await dotnetRef.invokeMethodAsync(getSignatureHelpMethod, position.lineNumber, position.column); + if (!signature) { + return undefined; + } + return { + value: signature.value, + dispose: () => { }, + } + } + }); + }, + CSharpLanguageRegisterHoverProvider: (dotnetRef, getHoverProviderMethod) => { + return monaco.languages.registerHoverProvider("csharp", { + provideHover: async (model, position, token, context) => { + var mess = await dotnetRef.invokeMethodAsync(getHoverProviderMethod, position.lineNumber, position.column); + return { + contents: [ + { + value: mess + } + ] + } + } + }); + } +} + +window.addEventListener("keydown", function (e) { + if ((e.key === 's' || e.key == 'S') && e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + } +}, false); + +window.addEventListener("contextmenu", function (e) { + e.preventDefault(); + e.stopPropagation(); +}, false); + +window.SetRadioChecked = function (id) { + document.getElementById(id).checked = true; +}; + +window.UncheckRadioAll = function (name) { + const radios = document.querySelectorAll(`input[type="radio"][name="${name}"]`); + radios.forEach(radio => radio.checked = false); +}; + +window.ScrollToBottom = (element) => { + if (element) { + element.scrollTop = element.scrollHeight; + } +}; diff --git a/RobotNet.WebApp/wwwroot/manifest.webmanifest b/RobotNet.WebApp/wwwroot/manifest.webmanifest new file mode 100644 index 0000000..df59020 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/manifest.webmanifest @@ -0,0 +1,22 @@ +{ + "name": "RobotNet.WebApp", + "short_name": "RobotNet.WebApp", + "id": "./", + "start_url": "./", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#03173d", + "prefer_related_applications": false, + "icons": [ + { + "src": "icon-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "icon-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/RobotNet.WebApp/wwwroot/sehc/amr.png b/RobotNet.WebApp/wwwroot/sehc/amr.png new file mode 100644 index 0000000000000000000000000000000000000000..b4ae207314130335d97e2a7b390348cc0aa53d8d GIT binary patch literal 106820 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 z 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�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 literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/sehc/station.png b/RobotNet.WebApp/wwwroot/sehc/station.png new file mode 100644 index 0000000000000000000000000000000000000000..d35a9a25ecd6ef776e2767ff283fc342a9890c59 GIT binary patch literal 47514 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 ajj?-ekoA^!(Pd5UKM literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/sehc/trolley.png b/RobotNet.WebApp/wwwroot/sehc/trolley.png new file mode 100644 index 0000000000000000000000000000000000000000..832f9779810162387b54983f756ea79dba6630a6 GIT binary patch literal 47708 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 z z)O+i0`>_leHS`7RLHhgF52^QibX@cp0op~X!RvQcU_ivD#i-QT38fzAhBUW8ct=j} zSU9=A^EU0C40c2t%gB8$WB7C5#E+ks(b>nFueUE8$dmR|Wo%+W$OTx-Qym@~W6;(K zD_6i`vMwoXg?KI`E1Px5J)Zfro|{SJv{G@=EP+d zQkK>}B1{au;Jh;VHDI(scUr#=E;Ijlj+ZRWe+*nhxZ8u3Xhx6ejokVNC2$H8F@0oQ z_Bq1vn8hY>tbDd-N;kfIA)qzH!VoYzr29}Wsx-|%rblgYC!39RIl{w9OKV@N90#jJ06^*U$~oWRl#9n;VbEYXgk}#vqTzwh z3Vq18EUiv6Fx=Mnv*e)svWGmYT^v(g-=lpz@HnFp|v+$??vcF6DU|G;H=7?pAAHNKf)Wu5Wm5^jdHga)m#q~vJ%(JC3d1zgBA*1v$ zM)6QBIg%v%{CtvRvI|*zb(9t7M#-TvokqQu(mKv24KUMb!c8Sidvc&cz-7lQWI==9 z*qM@@3@mEEuM}}9Ci2YZx(C``-rmum?I^9+4_-%7wQ1=C1R9x*M+2e!AX>7tvYrjR z!}s?G4$63=LFAf9t9)l#_~IMvf>+)@nR#=UrR2Q*`l_5h&mHy67-^A-8YQqe27{-AxcjR}=z!k1LA|_{{*i)b9 z>{T4XXhUGkA+L&$Z5dr|LD$VXE)T(g^nix$b}SgxyJeVR$1V~_Mz$(8o?`XL#o9nZ z;sC1ClG=8x$JG8VZsV3#0JZMeLH8PD@j{T)`H60e_rujhJ@Kd#$8FdH3fG-cu$Mkb zg6_Jz8PB1*&01F4Xe}8J=6ei~$H5Ew=h6m0+4&y>q&xZGYHZ7da(QPa)xfe+Y}zzC zHBMVRilp=i9Q)N*8)67)n!1q>chL(bFuSr$bXgx672o6r&BI*+LXO;*7&OhUuU+Ud zj5p0rm$%Q2FK4EZeUR4>ls8%jTl3}w$kOWUnU63(VRgg;b%;TFVG?)9DUvagF0ODj zFG|dMOL)AL!M->t?R{}FsazX)(o&WQscRa{x0LN|?7z-~k5xsTqZ=swOvp7g{GtdK zJ`^R^FhyU4@Wp4d#_|`j5X~D=5ibw>MH4V4EIo2 zQ!4ZD%;gB_1=hQ+dKj00O910Ok0f3d^*#InrT5uSR?I6+%B#+kGK7I|i{UgA#AQ@| zkT{o%xUu*G5{mg}x!Zk(51RzrH#!#FfV__m1CvEy2Dl{;%vn2{fv86-x^SD5$;isXw9Yk;ylEAu?e6KR;1E7a zYedv7Mr&W{5t~4>Mx6vhn8bQ*Jm?HY(Z^Z97?{Q*DO3M@lcfcP}8a#CGfoYWDF7X60Vlj>c- z`V9c~CF_Wa9E_E-xrQWRR39DbAjJ_O-2p;tGY!u9JgPngT)d=)5#lpf zNstlK2Qs6k@zsL@0KlgKA<)FDsTZ23wuKAwc6+PRjXr|)U;rqM8RjJiQ%~*!{Hr|$ zH3|&AfcF5rRXNn5D^7*AW(D+Z49#9%n!Y1E^ zLP8Fw$JuLsN>So|+=1Mk{_-xn|MNNi&B#kobZ(9sg(KpdF~j-|3HkDO>Z8MYuQ>r; zGcAu?u;LxDFOb5YlcpQN&!$|?nBDShg(N{grLAV^$lc5n(~>h4fp~2|INn(>9juf5 z!PkK$m?%HN2Q=5afUG=NUIq3Y`d?1~;0YN1S)dN?2aX&4=`!aa0qvrP~Z>E zfR%K1=Ny3S-bU?!1eHjR2-Lp+-2RCzupo!oAPx4*|4xrRBnT

pq6O+d}`m*$LLG~WtLWVfUe@4xG`nA3A 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 LS}!X=mS6r4{(g4P literal 0 HcmV?d00001 diff --git a/RobotNet.WebApp/wwwroot/service-worker.js b/RobotNet.WebApp/wwwroot/service-worker.js new file mode 100644 index 0000000..fe614da --- /dev/null +++ b/RobotNet.WebApp/wwwroot/service-worker.js @@ -0,0 +1,4 @@ +// In development, always fetch from the network and do not enable offline support. +// This is because caching would make development more difficult (changes would not +// be reflected on the first load after each change). +self.addEventListener('fetch', () => { }); diff --git a/RobotNet.WebApp/wwwroot/service-worker.published.js b/RobotNet.WebApp/wwwroot/service-worker.published.js new file mode 100644 index 0000000..1f7f543 --- /dev/null +++ b/RobotNet.WebApp/wwwroot/service-worker.published.js @@ -0,0 +1,55 @@ +// Caution! Be sure you understand the caveats before publishing an application with +// offline support. See https://aka.ms/blazor-offline-considerations + +self.importScripts('./service-worker-assets.js'); +self.addEventListener('install', event => event.waitUntil(onInstall(event))); +self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +const cacheNamePrefix = 'offline-cache-'; +const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; +const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'. +const base = "/"; +const baseUrl = new URL(base, self.origin); +const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href); + +async function onInstall(event) { + console.info('Service worker: Install'); + + // Fetch and cache all matching items from the assets manifest + const assetsRequests = self.assetsManifest.assets + .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) + .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) + .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); + await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +} + +async function onActivate(event) { + console.info('Service worker: Activate'); + + // Delete unused caches + const cacheKeys = await caches.keys(); + await Promise.all(cacheKeys + .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) + .map(key => caches.delete(key))); +} + +async function onFetch(event) { + let cachedResponse = null; + if (event.request.method === 'GET') { + // For all navigation requests, try to serve index.html from cache, + // unless that request is for an offline resource. + // If you need some URLs to be server-rendered, edit the following check to exclude those URLs + const shouldServeIndexHtml = event.request.mode === 'navigate' + && !manifestUrlList.some(url => url === event.request.url); + + const request = shouldServeIndexHtml ? 'index.html' : event.request; + const cache = await caches.open(cacheName); + cachedResponse = await cache.match(request); + } + + return cachedResponse || fetch(event.request); +} diff --git a/RobotNet.sln b/RobotNet.sln new file mode 100644 index 0000000..46c9642 --- /dev/null +++ b/RobotNet.sln @@ -0,0 +1,121 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.AppHost", "RobotNet.AppHost\RobotNet.AppHost.csproj", "{7C46123C-7EA2-4BCE-85DC-485A3C7CDAFF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.ServiceDefaults", "RobotNet.ServiceDefaults\RobotNet.ServiceDefaults.csproj", "{F3DA7256-CA09-57B2-4514-4587DCE6A102}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.IdentityServer", "RobotNet.IdentityServer\RobotNet.IdentityServer.csproj", "{5548B356-8739-46A2-8EB7-F0070620C3E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.ScriptManager", "RobotNet.ScriptManager\RobotNet.ScriptManager.csproj", "{0C50A3AA-A586-4085-8E6D-66AA020547F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.MapManager", "RobotNet.MapManager\RobotNet.MapManager.csproj", "{94BFC618-35F9-4433-B2F4-777591FC3316}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.Script", "RobotNet.Script\RobotNet.Script.csproj", "{12ADCF53-6421-4483-B674-73A75F72088E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.Script.Shares", "RobotNet.Script.Shares\RobotNet.Script.Shares.csproj", "{C2DC5F38-83BA-47BC-8704-958AF7F7DFCB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.MapShares", "RobotNet.MapShares\RobotNet.MapShares.csproj", "{B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.RobotManager", "RobotNet.RobotManager\RobotNet.RobotManager.csproj", "{2A0FCCB9-0F66-46BE-A766-36A457074430}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.RobotShares", "RobotNet.RobotShares\RobotNet.RobotShares.csproj", "{5E777F2B-EBD4-403D-94D5-30916FC2C676}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.OpenIddictClient", "RobotNet.OpenIddictClient\RobotNet.OpenIddictClient.csproj", "{AEFF3F5C-5B17-4C3E-9C8E-1390268767A9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shares", "Shares", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.Shares", "RobotNet.Shares\RobotNet.Shares.csproj", "{E0B35EDD-9E69-444B-A4AD-DD01356120A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.WebApp", "RobotNet.WebApp\RobotNet.WebApp.csproj", "{A0B344D8-8FE1-1944-6DB4-BC60CB94181A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.Clients", "RobotNet.Clients\RobotNet.Clients.csproj", "{3E60FD64-74FD-453F-9EEE-3CED9E23C051}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotNet.Script.Expressions", "RobotNet.Script.Expressions\RobotNet.Script.Expressions.csproj", "{7B5649BA-0EB4-460B-A084-F661E7FD868C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C46123C-7EA2-4BCE-85DC-485A3C7CDAFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C46123C-7EA2-4BCE-85DC-485A3C7CDAFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C46123C-7EA2-4BCE-85DC-485A3C7CDAFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C46123C-7EA2-4BCE-85DC-485A3C7CDAFF}.Release|Any CPU.Build.0 = Release|Any CPU + {F3DA7256-CA09-57B2-4514-4587DCE6A102}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3DA7256-CA09-57B2-4514-4587DCE6A102}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3DA7256-CA09-57B2-4514-4587DCE6A102}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3DA7256-CA09-57B2-4514-4587DCE6A102}.Release|Any CPU.Build.0 = Release|Any CPU + {5548B356-8739-46A2-8EB7-F0070620C3E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5548B356-8739-46A2-8EB7-F0070620C3E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5548B356-8739-46A2-8EB7-F0070620C3E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5548B356-8739-46A2-8EB7-F0070620C3E5}.Release|Any CPU.Build.0 = Release|Any CPU + {0C50A3AA-A586-4085-8E6D-66AA020547F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C50A3AA-A586-4085-8E6D-66AA020547F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C50A3AA-A586-4085-8E6D-66AA020547F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C50A3AA-A586-4085-8E6D-66AA020547F3}.Release|Any CPU.Build.0 = Release|Any CPU + {94BFC618-35F9-4433-B2F4-777591FC3316}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94BFC618-35F9-4433-B2F4-777591FC3316}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94BFC618-35F9-4433-B2F4-777591FC3316}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94BFC618-35F9-4433-B2F4-777591FC3316}.Release|Any CPU.Build.0 = Release|Any CPU + {12ADCF53-6421-4483-B674-73A75F72088E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12ADCF53-6421-4483-B674-73A75F72088E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12ADCF53-6421-4483-B674-73A75F72088E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12ADCF53-6421-4483-B674-73A75F72088E}.Release|Any CPU.Build.0 = Release|Any CPU + {C2DC5F38-83BA-47BC-8704-958AF7F7DFCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2DC5F38-83BA-47BC-8704-958AF7F7DFCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2DC5F38-83BA-47BC-8704-958AF7F7DFCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2DC5F38-83BA-47BC-8704-958AF7F7DFCB}.Release|Any CPU.Build.0 = Release|Any CPU + {B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C}.Release|Any CPU.Build.0 = Release|Any CPU + {2A0FCCB9-0F66-46BE-A766-36A457074430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A0FCCB9-0F66-46BE-A766-36A457074430}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A0FCCB9-0F66-46BE-A766-36A457074430}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A0FCCB9-0F66-46BE-A766-36A457074430}.Release|Any CPU.Build.0 = Release|Any CPU + {5E777F2B-EBD4-403D-94D5-30916FC2C676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E777F2B-EBD4-403D-94D5-30916FC2C676}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E777F2B-EBD4-403D-94D5-30916FC2C676}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E777F2B-EBD4-403D-94D5-30916FC2C676}.Release|Any CPU.Build.0 = Release|Any CPU + {AEFF3F5C-5B17-4C3E-9C8E-1390268767A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEFF3F5C-5B17-4C3E-9C8E-1390268767A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEFF3F5C-5B17-4C3E-9C8E-1390268767A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEFF3F5C-5B17-4C3E-9C8E-1390268767A9}.Release|Any CPU.Build.0 = Release|Any CPU + {E0B35EDD-9E69-444B-A4AD-DD01356120A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0B35EDD-9E69-444B-A4AD-DD01356120A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0B35EDD-9E69-444B-A4AD-DD01356120A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0B35EDD-9E69-444B-A4AD-DD01356120A4}.Release|Any CPU.Build.0 = Release|Any CPU + {A0B344D8-8FE1-1944-6DB4-BC60CB94181A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A0B344D8-8FE1-1944-6DB4-BC60CB94181A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A0B344D8-8FE1-1944-6DB4-BC60CB94181A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A0B344D8-8FE1-1944-6DB4-BC60CB94181A}.Release|Any CPU.Build.0 = Release|Any CPU + {3E60FD64-74FD-453F-9EEE-3CED9E23C051}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E60FD64-74FD-453F-9EEE-3CED9E23C051}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E60FD64-74FD-453F-9EEE-3CED9E23C051}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E60FD64-74FD-453F-9EEE-3CED9E23C051}.Release|Any CPU.Build.0 = Release|Any CPU + {7B5649BA-0EB4-460B-A084-F661E7FD868C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B5649BA-0EB4-460B-A084-F661E7FD868C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B5649BA-0EB4-460B-A084-F661E7FD868C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B5649BA-0EB4-460B-A084-F661E7FD868C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {F3DA7256-CA09-57B2-4514-4587DCE6A102} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {12ADCF53-6421-4483-B674-73A75F72088E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {C2DC5F38-83BA-47BC-8704-958AF7F7DFCB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {B2720E9D-1F2D-4AB2-A541-9B81FAB3C94C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {5E777F2B-EBD4-403D-94D5-30916FC2C676} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {AEFF3F5C-5B17-4C3E-9C8E-1390268767A9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E0B35EDD-9E69-444B-A4AD-DD01356120A4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {3E60FD64-74FD-453F-9EEE-3CED9E23C051} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {7B5649BA-0EB4-460B-A084-F661E7FD868C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {66C948A9-23CF-47BB-A933-72551E23BCE6} + EndGlobalSection +EndGlobal diff --git a/appsettings.RobotNet.WebApp.json b/appsettings.RobotNet.WebApp.json new file mode 100644 index 0000000..c1e5375 --- /dev/null +++ b/appsettings.RobotNet.WebApp.json @@ -0,0 +1,48 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "System.Net.Http.HttpClient": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "Local": { + "Authority": "https://172.20.235.2:8061", + "ClientId": "robotnet-webapp", + "ResponseType": "code", + "DefaultScopes": [ + "openid", + "profile", + "email", + "roles", + "robotnet-script-api", + "robotnet-robot-api", + "robotnet-map-api" + ], + "RedirectUri": "https://172.20.235.2:8035/authentication/login-callback", + "PostLogoutRedirectUri": "https://172.20.235.2:8035/authentication/logout-callback" + }, + "ScriptManager": { + "BaseAddress": "https://172.20.235.2:8102" + }, + "RobotManager": { + "BaseAddress": "https://172.20.235.2:8179" + }, + "MapManager": { + "BaseAddress": "https://172.20.235.2:8177" + }, + "Logs": [ + { + "Name": "RobotManager", + "Url": "https://172.20.235.2:8179/api/RobotManagerLogger" + }, + { + "Name": "MapManager", + "Url": "https://172.20.235.2:7177/api/MapDesignerLogger" + }, + { + "Name": "ScriptManager", + "Url": "https://172.20.235.2:8102/api/ScriptManagerLogger" + } + ] +} diff --git a/certificate/gencert.cmd b/certificate/gencert.cmd new file mode 100644 index 0000000..7cbb417 --- /dev/null +++ b/certificate/gencert.cmd @@ -0,0 +1,20 @@ +del /q /s .\*.pfx +del /q /s .\*.crt +del /q /s .\*.key +del /q /s .\*.pem +del /q /s .\*.srl +del /q /s .\*.csr + +openssl genrsa -out ca.key 2048 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 365000 -out ca.crt -subj "/CN=LocalCA" + +openssl genrsa -out identityserver.key 2048 +openssl req -new -key identityserver.key -out identityserver.csr -config san.cnf + +openssl x509 -req -in identityserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out identityserver.crt -days 365000 -sha256 -extensions v3_req -extfile san.cnf + +openssl pkcs12 -export -out robotnet.pfx -inkey identityserver.key -in identityserver.crt -certfile ca.crt -password pass:RobotNet@2024 + +openssl pkcs12 -in robotnet.pfx -out cert.pem -clcerts -nokeys -passin pass:RobotNet@2024 + +openssl pkcs12 -in robotnet.pfx -out key.pem -nocerts -nodes -passin pass:RobotNet@2024 diff --git a/certificate/gencert.sh b/certificate/gencert.sh new file mode 100644 index 0000000..44e3ab2 --- /dev/null +++ b/certificate/gencert.sh @@ -0,0 +1,23 @@ +#/bin/bash + +rm -f ./*.pfx +rm -f ./*.crt +rm -f ./*.key +rm -f ./*.pem +rm -f ./*.srl +rm -f ./*.csr + +#exit +openssl genrsa -out ca.key 2048 +openssl req -x509 -new -nodes -key ca.key -sha256 -days 365000 -out ca.crt -subj "/CN=LocalCA" + +openssl genrsa -out identityserver.key 2048 +openssl req -new -key identityserver.key -out identityserver.csr -config san.cnf + +openssl x509 -req -in identityserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out identityserver.crt -days 365000 -sha256 -extensions v3_req -extfile san.cnf + +openssl pkcs12 -export -out robotnet.pfx -inkey identityserver.key -in identityserver.crt -certfile ca.crt -password pass:RobotNet@2024 + +openssl pkcs12 -in robotnet.pfx -out cert.pem -clcerts -nokeys -passin pass:RobotNet@2024 + +openssl pkcs12 -in robotnet.pfx -out key.pem -nocerts -nodes -passin pass:RobotNet@2024 diff --git a/certificate/san.cnf b/certificate/san.cnf new file mode 100644 index 0000000..0c0ce36 --- /dev/null +++ b/certificate/san.cnf @@ -0,0 +1,10 @@ +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +CN = identityserver + +[v3_req] +subjectAltName = DNS:identityserver,DNS:mapmanager,DNS:mapdesigner,DNS:localhost,IP:172.20.235.2 diff --git a/clean.ps1 b/clean.ps1 new file mode 100644 index 0000000..c403464 --- /dev/null +++ b/clean.ps1 @@ -0,0 +1 @@ +Get-ChildItem -Path . -Include bin,obj -Directory -Recurse | Remove-Item -Recurse -Force diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..f6b1806 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,153 @@ +services: + identityserver: + container_name: identityserver + build: + context: . + dockerfile: RobotNet.IdentityServer/Dockerfile + image: ${DOCKER_HUB}/identityserver:${TAG} + restart: always + ports: + - "${IDENTITY_SERVER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=RobotNet.Identity;User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictCertificate__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/IdentityServer-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + + mapmanager: + container_name: mapmanager + build: + context: . + dockerfile: RobotNet.MapManager/Dockerfile + image: ${DOCKER_HUB}/mapmanager:${TAG} + restart: always + ports: + - "${MAP_MANAGER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=RobotNet.MapEditor;User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - MinIO__UsingLocal=false + - MinIO__Endpoint=${MINIO_IP}:9000 + - MinIO__Bucket=mapeditor + - MinIO__User=minio + - MinIO__Password=robotics + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/MapManager-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + + robotmanager: + container_name: robotmanager + build: + context: . + dockerfile: RobotNet.RobotManager/Dockerfile + image: ${DOCKER_HUB}/robotmanager:${TAG} + restart: always + ports: + - "${ROBOT_MANAGER_PORT}:443" + - "${MQTT_PORT}:1883" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=RobotNet.RobotEditor;User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - MinIO__UsingLocal=false + - MinIO__Endpoint=${MINIO_IP}:9000 + - MinIO__Bucket=mapeditor + - MinIO__User=minio + - MinIO__Password=robotics + - MapManager__Url=https://mapmanager:443 + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/RobotManager-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + mapmanager: + condition: service_started + + scriptmanager: + container_name: scriptmanager + build: + context: . + dockerfile: RobotNet.ScriptManager/Dockerfile + image: ${DOCKER_HUB}/scriptmanager:${TAG} + restart: always + ports: + - "${SCRIPT_MANAGER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=RobotNet.Scripts;User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - RobotManager__Url=https://robotmanager:443 + - MapManager__Url=https://mapmanager:443 + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./.scripts:/app/scripts + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/ScriptManager-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + mapmanager: + condition: service_started + robotmanager: + condition: service_started + + webapp: + container_name: webapp + build: + context: . + dockerfile: RobotNet.WebApp/Dockerfile + image: ${DOCKER_HUB}/webapp:${TAG} + restart: always + ports: + - "${WEB_APP_PORT}:443" + environment: + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./appsettings.RobotNet.WebApp.json:/usr/share/nginx/html/appsettings.json:ro + - ./certificate/cert.pem:/etc/nginx/cert.pem:ro + - ./certificate/key.pem:/etc/nginx/key.pem:ro + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + mapmanager: + condition: service_started + robotmanager: + condition: service_started + scriptmanager: + condition: service_started + +networks: + robotnet: + driver: bridge diff --git a/docker-deploy.yaml b/docker-deploy.yaml new file mode 100644 index 0000000..1340e71 --- /dev/null +++ b/docker-deploy.yaml @@ -0,0 +1,188 @@ +services: + database: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: robotnet-database + restart: unless-stopped + ports: + - 1433:1433 + volumes: + - ./database:/var/opt/mssql/data + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=${SQL_PASSWORD} + - TZ=Asia/Ho_Chi_Minh + networks: + - robotnet + + minio: + image: minio/minio + container_name: minio + volumes: + - "./minio:/minio-data/data" + ports: + - "9090:9090" + environment: + - MINIO_ROOT_USER=${MINIO_ROOT_USER} + - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + - TZ=Asia/Ho_Chi_Minh + command: server --console-address ":9090" /minio-data/data + restart: always + networks: + - robotnet + + identityserver: + container_name: identityserver + image: ${DOCKER_HUB}/identityserver:${TAG} + restart: always + ports: + - "${IDENTITY_SERVER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=${SQL_IDENTITY_DB};User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictCertificate__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/IdentityServer-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + database: + condition: service_started + + mapmanager: + container_name: mapmanager + image: ${DOCKER_HUB}/mapmanager:${TAG} + restart: always + ports: + - "${MAP_MANAGER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=${SQL_MAP_MANAGER_DB};User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - MinIO__UsingLocal=false + - MinIO__Endpoint=${MINIO_IP}:9000 + - MinIO__Bucket=mapeditor + - MinIO__User=${MINIO_ROOT_USER} + - MinIO__Password=${MINIO_ROOT_PASSWORD} + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/MapManager-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + database: + condition: service_started + minio: + condition: service_started + + robotmanager: + container_name: robotmanager + image: ${DOCKER_HUB}/robotmanager:${TAG} + restart: always + ports: + - "${ROBOT_MANAGER_PORT}:443" + - "${MQTT_PORT}:1883" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=${SQL_ROBOT_MANAGER_DB};User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - PathPlanning__Type=None + - MinIO__UsingLocal=false + - MinIO__Endpoint=${MINIO_IP}:9000 + - MinIO__Bucket=mapeditor + - MinIO__User=${MINIO_ROOT_USER} + - MinIO__Password=${MINIO_ROOT_PASSWORD} + - MapManager__Url=https://mapmanager:443 + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./DataProtections/RobotManager-Keys:/root/.aspnet/DataProtection-Keys + networks: + - robotnet + depends_on: + database: + condition: service_started + minio: + condition: service_started + identityserver: + condition: service_started + mapmanager: + condition: service_started + + scriptmanager: + container_name: scriptmanager + image: ${DOCKER_HUB}/scriptmanager:${TAG} + restart: always + ports: + - "${SCRIPT_MANAGER_PORT}:443" + environment: + - ConnectionStrings__DefaultConnection=Server=${SQL_IP};Database=${SQL_SCRIPT_MANAGER_DB};User Id=sa;Password=${SQL_PASSWORD};TrustServerCertificate=True;MultipleActiveResultSets=true; + - Kestrel__Certificates__Default__Path=/app/certs/robotnet.pfx + - Kestrel__Certificates__Default__Password=${CERT_PASSWORD} + - Kestrel__Endpoints__https__Url=https://0.0.0.0:443 + - OpenIddictClientProviderOptions__Issuer=https://${HOST_IP}:${IDENTITY_SERVER_PORT} + - RobotManager__Url=https://robotmanager:443 + - MapManager__Url=https://mapmanager:443 + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./.scripts:/app/scripts + - ./certificate/robotnet.pfx:/app/certs/robotnet.pfx:ro + - ./certificate/ca.crt:/usr/local/share/ca-certificates/ca.crt:ro + - ./logs/scriptmanager/logs:/app/logs + - ./logs/scriptmanager/plogs:/app/plogs + - ./DataProtections/ScriptManager-Keys:/root/.aspnet/DataProtection-Keys + devices: + - /dev/robotnet/*:/dev/robotnet/* + privileged: true + networks: + - robotnet + depends_on: + database: + condition: service_started + identityserver: + condition: service_started + mapmanager: + condition: service_started + robotmanager: + condition: service_started + + webapp: + container_name: webapp + image: ${DOCKER_HUB}/webapp:${TAG} + restart: always + ports: + - "${WEB_APP_PORT}:443" + environment: + - TZ=Asia/Ho_Chi_Minh + volumes: + - ./appsettings.RobotNet.WebApp.json:/usr/share/nginx/html/appsettings.json:ro + - ./certificate/cert.pem:/etc/nginx/cert.pem:ro + - ./certificate/key.pem:/etc/nginx/key.pem:ro + networks: + - robotnet + depends_on: + identityserver: + condition: service_started + mapmanager: + condition: service_started + robotmanager: + condition: service_started + scriptmanager: + condition: service_started + +networks: + robotnet: + driver: bridge diff --git a/install.md b/install.md new file mode 100644 index 0000000..062a308 --- /dev/null +++ b/install.md @@ -0,0 +1,85 @@ +### Prepare on install server +cd ~ +mkdir robotnet +cd robotnet +mkdir .scripts +mkdir certificate +mkdir database +mkdir DataProtections +mkdir minio +mkdir logs +mkdir logs/scriptmanager +mkdir logs/scriptmanager/logs +mkdir logs/scriptmanager/plogs + +### Copy từ máy tính clone git RobotNet +scp ./certificate/san.cnf [username]@[IP]:~/robotnet/certificate/ +scp ./certificate/gencert.sh [username]@[IP]:~/robotnet/certificate/ +scp .env [username]@[IP]:~/robotnet/ +scp docker-deploy.yaml [username]@[IP]:~/robotnet/docker-compose.yaml +scp appsettings.RobotNet.WebApp.json [username]@[IP]:~/robotnet/appsettings.RobotNet.WebApp.json + +### Cài đặt trên server + +## Thêm domain robotics.doc -> 172.20.235.176 vào file /etc/hosts +## Thêm insecure-registries vào docker /etc/docker/daemon.json +{ + "insecure-registries" : [ "robotics.doc", "robotics.doc:8083" ] +} + +## Restart docker +sudo systemctl daemon-reload +sudo systemctl restart docker + +## Login docker.rob +docker login robotics.doc:8083 + +## Thêm host ip vào subjectAltName của file ~/robotnet/certificate/san.cnf + +## Tạo chứng chỉ +sed -i 's/\r$//' gencert.sh +sed -i 's/\r$//' san.cnf +chmod +x ./gencert.sh +./gencert.sh + +## Cập nhật các thông số trong file .env +# TAG : version hiện tại +# HOST_IP: địa chỉ IP dùng để kết nối +# WEB_APP_PORT: 443 +# SQL_IP: database +# MINIO_IP: minio + +cd ~/robotnet +nano .env + +## Cập nhật các thông số trong file appsettings.RobotNet.WebApp.json +# Local -> Authority : cập nhật IP của server +# Local -> RedirectUri: cập nhật IP của server, nếu dùng port 443 thì bỏ khai báo port +# Local -> PostLogoutRedirectUri: cập nhật IP của server, nếu dùng port 443 thì bỏ khai báo port +# ScriptManager -> BaseAddress : cập nhật IP của server +# RobotManager -> BaseAddress : cập nhật IP của server +# MapManager -> BaseAddress : cập nhật IP của server + +cd ~/robotnet +nano appsettings.RobotNet.WebApp.json + +### thêm quyền ghi vào folder database +cd ~/robotnet +chmod a+w database + +### Start server +cd ~/robotnet +docker compose up -d + +## Truy cập vào web identity server (port 8061) đển cập nhập redirect url + + +## Restart server +cd ~/robotnet +docker compose restart + +### Stop server +cd ~/robotnet +docker compose down + +### Cập nhật url redirect của webapp client trong identity diff --git a/sehcio/Makefile b/sehcio/Makefile new file mode 100644 index 0000000..53bbc03 --- /dev/null +++ b/sehcio/Makefile @@ -0,0 +1,21 @@ +obj-m += sehcio.o + +KDIR := /home/anhnv/robotnet/kernels/linux-6.12.39 +KBUILD_EXTRA_SYMBOLS := /home/anhnv/robotnet/ethercat/Module.symvers +ccflags-y := -I/home/anhnv/robotnet/ethercat/include + +SIGN_SCRIPT := $(KDIR)/scripts/sign-file +SIGN_KEY := $(KDIR)/certs/signing_key.pem +SIGN_CERT := $(KDIR)/certs/signing_key.x509 + +all: + make -C $(KDIR) M=$(PWD) modules + @if [ -f $(SIGN_SCRIPT) ] && [ -f $(SIGN_KEY) ] && [ -f $(SIGN_CERT) ]; then \ + echo "Signing module sehcio.ko..."; \ + $(SIGN_SCRIPT) sha256 $(SIGN_KEY) $(SIGN_CERT) sehcio.ko; \ + else \ + echo "Warning: Module signing files not found, skipping signing"; \ + fi + +clean: + make -C $(KDIR) M=$(PWD) clean diff --git a/sehcio/sehcio.c b/sehcio/sehcio.c new file mode 100644 index 0000000..2721a05 --- /dev/null +++ b/sehcio/sehcio.c @@ -0,0 +1,449 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ecrt.h" + +// Module parameters +#define FREQUENCY 100 +#define PFX "sehc_io: " +#define DEVICE_NAME "sehcio" +#define CLASS_NAME "robotnet" + +/* Master 0, Slave 0, "Ezi-IO EtherCAT INPUT16" + * Vendor ID: 0x0fa00000 + * Product code: 0x00002002 + * Revision number: 0x00000001 + */ + +ec_pdo_entry_info_t slave_0_pdo_entries[] = { + {0x6000, 0x00, 1}, /* Bit0 */ + {0x6001, 0x00, 1}, /* Bit1 */ + {0x6002, 0x00, 1}, /* Bit2 */ + {0x6003, 0x00, 1}, /* Bit3 */ + {0x6004, 0x00, 1}, /* Bit4 */ + {0x6005, 0x00, 1}, /* Bit5 */ + {0x6006, 0x00, 1}, /* Bit6 */ + {0x6007, 0x00, 1}, /* Bit7 */ + {0x6008, 0x00, 1}, /* Bit8 */ + {0x6009, 0x00, 1}, /* Bit9 */ + {0x600a, 0x00, 1}, /* Bit10 */ + {0x600b, 0x00, 1}, /* Bit11 */ + {0x600c, 0x00, 1}, /* Bit12 */ + {0x600d, 0x00, 1}, /* Bit13 */ + {0x600e, 0x00, 1}, /* Bit14 */ + {0x600f, 0x00, 1}, /* Bit15 */ +}; + +ec_pdo_info_t slave_0_pdos[] = { + {0x1a00, 16, slave_0_pdo_entries + 0}, /* DI */ +}; + +ec_sync_info_t slave_0_syncs[] = { + {0, EC_DIR_INPUT, 1, slave_0_pdos + 0, EC_WD_DISABLE}, + {0xff} +}; + +/* Master 0, Slave 1, "Ezi-IO EtherCAT OUTPUT16" + * Vendor ID: 0x0fa00000 + * Product code: 0x00002012 + * Revision number: 0x00000001 + */ + +ec_pdo_entry_info_t slave_1_pdo_entries[] = { + {0x7000, 0x00, 1}, /* Bit0 */ + {0x7001, 0x00, 1}, /* Bit1 */ + {0x7002, 0x00, 1}, /* Bit2 */ + {0x7003, 0x00, 1}, /* Bit3 */ + {0x7004, 0x00, 1}, /* Bit4 */ + {0x7005, 0x00, 1}, /* Bit5 */ + {0x7006, 0x00, 1}, /* Bit6 */ + {0x7007, 0x00, 1}, /* Bit7 */ + {0x7008, 0x00, 1}, /* Bit8 */ + {0x7009, 0x00, 1}, /* Bit9 */ + {0x700a, 0x00, 1}, /* Bit10 */ + {0x700b, 0x00, 1}, /* Bit11 */ + {0x700c, 0x00, 1}, /* Bit12 */ + {0x700d, 0x00, 1}, /* Bit13 */ + {0x700e, 0x00, 1}, /* Bit14 */ + {0x700f, 0x00, 1}, /* Bit15 */ +}; + +ec_pdo_info_t slave_1_pdos[] = { + {0x1600, 8, slave_1_pdo_entries + 0}, /* DO1 */ + {0x1601, 8, slave_1_pdo_entries + 8}, /* DO2 */ +}; + +ec_sync_info_t slave_1_syncs[] = { + {0, EC_DIR_OUTPUT, 1, slave_1_pdos + 0, EC_WD_ENABLE}, + {1, EC_DIR_OUTPUT, 1, slave_1_pdos + 1, EC_WD_ENABLE}, + {0xff} +}; + +// EtherCAT variables +static ec_master_t *master = NULL; +static ec_master_state_t master_state = {}; +static spinlock_t master_spinlock; +static ec_domain_t *domain1 = NULL; +static ec_domain_state_t domain1_state = {}; +static uint8_t *domain1_pd; + +static unsigned int counter = 0; +static struct timer_list timer; + +static uint16_t din; +static uint16_t dout; + +static unsigned int off_d_in; +static unsigned int off_d_out_1; +static unsigned int off_d_out_2; + +static const ec_pdo_entry_reg_t domain1_regs[] = { + {0, 0, 0x0fa00000, 0x00002002, 0x6000, 0x00, &off_d_in}, // 16 bits cho input + {0, 1, 0x0fa00000, 0x00002012, 0x7000, 0x00, &off_d_out_1}, // 8 bits đầu + {0, 1, 0x0fa00000, 0x00002012, 0x7008, 0x00, &off_d_out_2}, // 8 bits sau + {} +}; + +// Character device variables +static int major_number; +static struct class *robotnet_class = NULL; +static struct device *sehcio_device = NULL; +static struct cdev sehcio_cdev; +static dev_t dev_number; + +// Function prototypes +void check_domain1_state(void); +void check_master_state(void); +void cyclic_task(struct timer_list *); +void send_callback(void *); +void receive_callback(void *); + +static int sehcio_open(struct inode *inode, struct file *file); +static int sehcio_release(struct inode *inode, struct file *file); +static ssize_t sehcio_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos); +static ssize_t sehcio_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos); +static char *robotnet_devnode(const struct device *dev, umode_t *mode); + +static const struct file_operations sehcio_fops = { + .owner = THIS_MODULE, + .open = sehcio_open, + .release = sehcio_release, + .read = sehcio_read, + .write = sehcio_write, +}; + +static int __init sehcio_init(void) +{ + int ret = -1; + ec_slave_config_t *sc; + unsigned int size; + + printk(KERN_INFO PFX "Starting...\n"); + + // Allocate major number + ret = alloc_chrdev_region(&dev_number, 0, 1, DEVICE_NAME); + if (ret < 0) { + printk(KERN_ERR PFX "Failed to allocate major number\n"); + goto out_return; + } + major_number = MAJOR(dev_number); + + // Initialize cdev + cdev_init(&sehcio_cdev, &sehcio_fops); + sehcio_cdev.owner = THIS_MODULE; + + // Add cdev to system + ret = cdev_add(&sehcio_cdev, dev_number, 1); + if (ret < 0) { + printk(KERN_ERR PFX "Failed to add cdev\n"); + goto out_unregister_chrdev; + } + + // Create device class + robotnet_class = class_create(CLASS_NAME); + if (IS_ERR(robotnet_class)) { + printk(KERN_ERR PFX "Failed to create class %s\n", CLASS_NAME); + ret = PTR_ERR(robotnet_class); + goto out_cdev_del; + } + + // Set devnode callback to create subdirectory + robotnet_class->devnode = robotnet_devnode; + + // Create device + sehcio_device = device_create(robotnet_class, NULL, dev_number, NULL, DEVICE_NAME); + if (IS_ERR(sehcio_device)) { + printk(KERN_ERR PFX "Failed to create device %s\n", DEVICE_NAME); + ret = PTR_ERR(sehcio_device); + goto out_class_destroy; + } + + printk(KERN_INFO PFX "Device /dev/%s/%s created\n", CLASS_NAME, DEVICE_NAME); + + // EtherCAT initialization + master = ecrt_request_master(0); + if (!master) { + ret = -EBUSY; + printk(KERN_ERR PFX "Requesting master 0 failed.\n"); + goto out_device_destroy; + } + + spin_lock_init(&master_spinlock); + ecrt_master_callbacks(master, send_callback, receive_callback, master); + + printk(KERN_INFO PFX "Registering domain...\n"); + if (!(domain1 = ecrt_master_create_domain(master))) { + printk(KERN_ERR PFX "Domain creation failed!\n"); + goto out_release_master; + } + + if (!(sc = ecrt_master_slave_config(master, 0, 0, 0x0fa00000, 0x00002002))) { + printk(KERN_ERR PFX "Failed to get slave 0-0 configuration.\n"); + goto out_release_master; + } + + if (ecrt_slave_config_pdos(sc, EC_END, slave_0_syncs)) { + printk(KERN_ERR PFX "Failed to configure 0-0 PDOs.\n"); + goto out_release_master; + } + + if (!(sc = ecrt_master_slave_config(master, 0, 1, 0x0fa00000, 0x00002012))) { + printk(KERN_ERR PFX "Failed to get slave 0-1 configuration.\n"); + goto out_release_master; + } + + if (ecrt_slave_config_pdos(sc, EC_END, slave_1_syncs)) { + printk(KERN_ERR PFX "Failed to configure 0-1 PDOs.\n"); + goto out_release_master; + } + + printk(KERN_INFO PFX "Registering PDO entries...\n"); + if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) { + printk(KERN_ERR PFX "PDO entry registration failed!\n"); + goto out_release_master; + } + + if ((size = ecrt_domain_size(domain1))) { + if (!(domain1_pd = (uint8_t *) kmalloc(size, GFP_KERNEL))) { + printk(KERN_ERR PFX "Failed to allocate %u bytes of process data memory!\n", size); + goto out_release_master; + } + ecrt_domain_external_memory(domain1, domain1_pd); + } + + printk(KERN_INFO PFX "Activating master...\n"); + if (ecrt_master_activate(master)) { + printk(KERN_ERR PFX "Failed to activate master!\n"); + goto out_free_process_data; + } + + printk(KERN_INFO PFX "Starting cyclic sample thread.\n"); + timer_setup(&timer, cyclic_task, 0); + timer.expires = jiffies + 10; + add_timer(&timer); + + printk(KERN_INFO PFX "Started.\n"); + return 0; + +out_free_process_data: + kfree(domain1_pd); +out_release_master: + ecrt_release_master(master); +out_device_destroy: + device_destroy(robotnet_class, dev_number); +out_class_destroy: + class_destroy(robotnet_class); +out_cdev_del: + cdev_del(&sehcio_cdev); +out_unregister_chrdev: + unregister_chrdev_region(dev_number, 1); +out_return: + printk(KERN_ERR PFX "Failed to load. Aborting.\n"); + return ret; +} + +void check_domain1_state(void) +{ + ec_domain_state_t ds; + + spin_lock(&master_spinlock); + ecrt_domain_state(domain1, &ds); + spin_unlock(&master_spinlock); + + if (ds.working_counter != domain1_state.working_counter) + printk(KERN_INFO PFX "Domain1: WC %u.\n", ds.working_counter); + if (ds.wc_state != domain1_state.wc_state) + printk(KERN_INFO PFX "Domain1: State %u.\n", ds.wc_state); + + domain1_state = ds; +} + +void check_master_state(void) +{ + ec_master_state_t ms; + + spin_lock(&master_spinlock); + ecrt_master_state(master, &ms); + spin_unlock(&master_spinlock); + + if (ms.slaves_responding != master_state.slaves_responding) + printk(KERN_INFO PFX "%u slave(s).\n", ms.slaves_responding); + if (ms.al_states != master_state.al_states) + printk(KERN_INFO PFX "AL states: 0x%02X.\n", ms.al_states); + if (ms.link_up != master_state.link_up) + printk(KERN_INFO PFX "Link is %s.\n", ms.link_up ? "up" : "down"); + + master_state = ms; +} + +void cyclic_task(struct timer_list *t) +{ + // receive process data + spin_lock(&master_spinlock); + ecrt_master_receive(master); + ecrt_domain_process(domain1); + spin_unlock(&master_spinlock); + + // check process data state (optional) + check_domain1_state(); + + if (counter) { + counter--; + } else { // do this at 1 Hz + counter = FREQUENCY; + check_master_state(); + } + + // read/write process data + din = EC_READ_U16(domain1_pd + off_d_in); + EC_WRITE_U8(domain1_pd + off_d_out_1, (dout >> 8) & 0xFF); + EC_WRITE_U8(domain1_pd + off_d_out_2, dout & 0xFF); + + // send process data + spin_lock(&master_spinlock); + ecrt_domain_queue(domain1); + ecrt_master_send(master); + spin_unlock(&master_spinlock); + + // restart timer + timer.expires += HZ / FREQUENCY; + add_timer(&timer); +} + +void send_callback(void *cb_data) +{ + ec_master_t *m = (ec_master_t *) cb_data; + spin_lock_bh(&master_spinlock); + ecrt_master_send_ext(m); + spin_unlock_bh(&master_spinlock); +} + +void receive_callback(void *cb_data) +{ + ec_master_t *m = (ec_master_t *) cb_data; + spin_lock_bh(&master_spinlock); + ecrt_master_receive(m); + spin_unlock_bh(&master_spinlock); +} + + +// Character device file operations +static int sehcio_open(struct inode *inode, struct file *file) +{ + printk(KERN_INFO PFX "Device opened\n"); + return 0; +} + +static int sehcio_release(struct inode *inode, struct file *file) +{ + printk(KERN_INFO PFX "Device closed\n"); + return 0; +} + +static ssize_t sehcio_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) +{ + uint16_t data; + + if (*f_pos >= 2) + return 0; // EOF + + if (count < 2) + return -EINVAL; + + data = din; + + if (copy_to_user(buf, &data, 2)) + return -EFAULT; + + *f_pos += 2; + return 2; +} + +static ssize_t sehcio_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) +{ + uint16_t data; + + if (count < 2) + return -EINVAL; + + if (copy_from_user(&data, buf, 2)) + return -EFAULT; + + dout = data; + + return 2; +} + + +static char *robotnet_devnode(const struct device *dev, umode_t *mode) +{ + return kasprintf(GFP_KERNEL, "%s/%s", CLASS_NAME, dev_name(dev)); +} + + +static void __exit sehcio_exit(void) +{ + printk(KERN_INFO PFX "Stopping...\n"); + del_timer_sync(&timer); + + if (master) { + printk(KERN_INFO PFX "Releasing master...\n"); + ecrt_release_master(master); + } + + if (domain1_pd) { + kfree(domain1_pd); + } + + // Remove device and class + if (sehcio_device) { + device_destroy(robotnet_class, dev_number); + } + + if (robotnet_class) { + class_destroy(robotnet_class); + } + + // Remove character device + cdev_del(&sehcio_cdev); + unregister_chrdev_region(dev_number, 1); + + printk(KERN_INFO PFX "Unloading.\n"); +} + + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("anhnv@phenikaa-x.com"); +MODULE_DESCRIPTION("SEHC I/O kernel module"); +MODULE_VERSION("0.1"); + +module_init(sehcio_init); +module_exit(sehcio_exit); \ No newline at end of file