diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2be2d74..1fa5705 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,8 +6,8 @@ "--name", "Basestation_Software", // Configure container to use the same network stack as the host machine. - "--network", - "host", + // "--network", + // "host", // Grants permission to the container to access USBs. "--privileged" ], @@ -41,7 +41,26 @@ // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + "forwardPorts": [ + 11000, + 12000 + ], + + "portsAttributes": { + "11000": { + "label": "RoveComm UDP", + "onAutoForward": "notify" + }, + "12000": { + "label": "RoveComm TCP", + "onAutoForward": "notify" + } + }, + + "appPort": [ + "11000:11000/udp", + "12000:12000/tcp" + ], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "dotnet tool install --global dotnet-ef --verbosity diagnostic && dotnet tool install --global dotnet-aspnet-codegenerator --verbosity diagnostic", diff --git a/.editorconfig b/.editorconfig index 633583f..414ce27 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ root = true # Indentation and spacing indent_size = 4 -indent_style = tab +indent_style = space tab_width = 4 # New line preferences @@ -56,13 +56,13 @@ dotnet_style_prefer_conditional_expression_over_assignment = true dotnet_style_prefer_conditional_expression_over_return = true dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_inferred_tuple_names = true:silent dotnet_style_prefer_is_null_check_over_reference_equality_method = false dotnet_style_prefer_simplified_boolean_expressions = true dotnet_style_prefer_simplified_interpolation = true # Field preferences -dotnet_style_readonly_field = true +dotnet_style_readonly_field = false:silent # Parameter preferences dotnet_code_quality_unused_parameters = all @@ -72,7 +72,7 @@ dotnet_remove_unnecessary_suppression_exclusions = none # New line preferences dotnet_style_allow_multiple_blank_lines_experimental = false -dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion +dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### @@ -82,14 +82,14 @@ csharp_style_var_for_built_in_types = false:suggestion csharp_style_var_when_type_is_apparent = false # Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true:silent @@ -97,24 +97,24 @@ csharp_style_pattern_matching_over_is_with_cast_check = true csharp_style_prefer_extended_property_pattern = true csharp_style_prefer_not_pattern = false:silent csharp_style_prefer_pattern_matching = false -csharp_style_prefer_switch_expression = true +csharp_style_prefer_switch_expression = true:silent # Null-checking preferences csharp_style_conditional_delegate_call = true # Modifier preferences -csharp_prefer_static_anonymous_function = true -csharp_prefer_static_local_function = true +csharp_prefer_static_anonymous_function = false:silent +csharp_prefer_static_local_function = true:silent csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async -csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct = false:silent csharp_style_prefer_readonly_struct_member = true # Code-block preferences csharp_prefer_braces = when_multiline:suggestion -csharp_prefer_simple_using_statement = true:suggestion -csharp_style_namespace_declarations = block_scoped:silent -csharp_style_prefer_method_group_conversion = true:silent -csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_simple_using_statement = false +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = false csharp_style_prefer_top_level_statements = false:warning # Expression-level preferences @@ -201,14 +201,14 @@ dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - dotnet_naming_rule.private_or_internal_field_should_be_camel_case__begins_with__.severity = suggestion dotnet_naming_rule.private_or_internal_field_should_be_camel_case__begins_with__.symbols = private_or_internal_field dotnet_naming_rule.private_or_internal_field_should_be_camel_case__begins_with__.style = camel_case__begins_with__ +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface @@ -243,14 +243,3 @@ dotnet_naming_style.camel_case__begins_with__.required_prefix = _ dotnet_naming_style.camel_case__begins_with__.required_suffix = dotnet_naming_style.camel_case__begins_with__.word_separator = dotnet_naming_style.camel_case__begins_with__.capitalization = camel_case - -[*.{cs,vb}] -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = false:suggestion -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_object_initializer = true:suggestion \ No newline at end of file diff --git a/.gitignore b/.gitignore index c32f716..60d7368 100644 --- a/.gitignore +++ b/.gitignore @@ -262,4 +262,6 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc \ No newline at end of file +*.pyc +/.vs/ProjectEvaluation/basestation_software.metadata.v7.bin +/.vs/ProjectEvaluation/basestation_software.projects.v7.bin diff --git a/Basestation_Software.Api/Controllers/GPSWaypointController.cs b/Basestation_Software.Api/Controllers/GPSWaypointController.cs index 0b9779d..84e35f6 100644 --- a/Basestation_Software.Api/Controllers/GPSWaypointController.cs +++ b/Basestation_Software.Api/Controllers/GPSWaypointController.cs @@ -1,111 +1,109 @@ using Basestation_Software.Api.Entities; using Basestation_Software.Models.Geospatial; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Basestation_Software.Api.Controllers +namespace Basestation_Software.Api.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class GPSWaypointController : ControllerBase { - [Route("api/[controller]")] - [ApiController] - public class GPSWaypointController : ControllerBase + // Declare member variables. + private readonly IGPSWaypointRepository _GPSWaypointRepository; + + /// + /// Constructor + /// + /// Implicitly passed in. + public GPSWaypointController(IGPSWaypointRepository GPSWaypointRepository) { - // Declare member variables. - private readonly IGPSWaypointRepository _GPSWaypointRepository; + _GPSWaypointRepository = GPSWaypointRepository; + } - /// - /// Constructor - /// - /// Implicitly passed in. - public GPSWaypointController(IGPSWaypointRepository GPSWaypointRepository) + /// + /// IN-Code API Endpoint for adding a waypoint to the DB. + /// + /// The waypoint object. + /// The API response object. + [HttpPut] + public async Task AddGPSWaypoint(GPSWaypoint waypoint) + { + GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.AddGPSWaypoint(waypoint); + if (dbWaypoint is not null) { - _GPSWaypointRepository = GPSWaypointRepository; + return Ok(); } - - /// - /// IN-Code API Endpoint for adding a waypoint to the DB. - /// - /// The waypoint object. - /// The API response object. - [HttpPut] - public async Task AddGPSWaypoint(GPSWaypoint waypoint) + else { - GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.AddGPSWaypoint(waypoint); - if (dbWaypoint is not null) - { - return Ok(); - } - else - { - return BadRequest(); - } + return BadRequest(); } + } - /// - /// IN-Code API Endpoint for removing a waypoint from the DB. - /// - /// The waypoint ID. - /// The API response object. - [HttpDelete("{waypointID}")] - public async Task DeleteGPSWaypoint(int waypointID) + /// + /// IN-Code API Endpoint for removing a waypoint from the DB. + /// + /// The waypoint ID. + /// The API response object. + [HttpDelete("{waypointID}")] + public async Task DeleteGPSWaypoint(int waypointID) + { + GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.DeleteGPSWaypoint(waypointID); + if (dbWaypoint is not null) { - GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.DeleteGPSWaypoint(waypointID); - if (dbWaypoint is not null) - { - return Ok(); - } - else - { - return NotFound(); - } + return Ok(); } - - /// - /// IN-Code API Endpoint for getting a waypoint to the DB. - /// - /// The waypoint id. - /// The API response object. - [HttpGet("{waypointID}")] - public async Task GetGPSWaypoint(int waypointID) + else { - GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.GetGPSWaypoint(waypointID); - if (dbWaypoint is not null) - { - return Ok(dbWaypoint); - } - else - { - return NotFound(); - } + return NotFound(); } + } - /// - /// IN-Code API Endpoint for getting all waypoints from the DB. - /// - /// The API response object. - [HttpGet] - public async Task GetAllGPSWaypoints() + /// + /// IN-Code API Endpoint for getting a waypoint to the DB. + /// + /// The waypoint id. + /// The API response object. + [HttpGet("{waypointID}")] + public async Task GetGPSWaypoint(int waypointID) + { + GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.GetGPSWaypoint(waypointID); + if (dbWaypoint is not null) { - return Ok(await _GPSWaypointRepository.GetAllGPSWaypoints()); + return Ok(dbWaypoint); } - - /// - /// IN-Code API Endpoint for updating a waypoint to the DB. - /// - /// The waypoint object. - /// The API response object. - [HttpPost] - public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) + else { - GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.UpdateGPSWaypoint(waypoint); - if (dbWaypoint is not null) - { - return Ok(dbWaypoint); - } - else - { - return NotFound(); - } + return NotFound(); } + } + /// + /// IN-Code API Endpoint for getting all waypoints from the DB. + /// + /// The API response object. + [HttpGet] + public async Task GetAllGPSWaypoints() + { + return Ok(await _GPSWaypointRepository.GetAllGPSWaypoints()); + } + + /// + /// IN-Code API Endpoint for updating a waypoint to the DB. + /// + /// The waypoint object. + /// The API response object. + [HttpPost] + public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) + { + GPSWaypoint? dbWaypoint = await _GPSWaypointRepository.UpdateGPSWaypoint(waypoint); + if (dbWaypoint is not null) + { + return Ok(dbWaypoint); + } + else + { + return NotFound(); + } } + } diff --git a/Basestation_Software.Api/Controllers/MapTileController.cs b/Basestation_Software.Api/Controllers/MapTileController.cs index f85ef81..dfbb1f7 100644 --- a/Basestation_Software.Api/Controllers/MapTileController.cs +++ b/Basestation_Software.Api/Controllers/MapTileController.cs @@ -1,111 +1,109 @@ using Basestation_Software.Api.Entities; using Basestation_Software.Models.Geospatial; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -namespace Basestation_Software.Api.Controllers +namespace Basestation_Software.Api.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class MapTilesController : ControllerBase { - [Route("api/[controller]")] - [ApiController] - public class MapTilesController : ControllerBase + // Declare member variables. + private readonly IMapTileRepository _TileRepository; + + /// + /// Constructor + /// + /// + public MapTilesController(IMapTileRepository tileRepository) { - // Declare member variables. - private readonly IMapTileRepository _TileRepository; + _TileRepository = tileRepository; + } - /// - /// Constructor - /// - /// - public MapTilesController(IMapTileRepository tileRepository) + [HttpPut] + public async Task AddMapTile(MapTile tile) + { + MapTile? dbTile = await _TileRepository.AddMapTile(tile); + if (dbTile is not null) { - _TileRepository = tileRepository; + return Ok(); } - - [HttpPut] - public async Task AddMapTile(MapTile tile) + else { - MapTile? dbTile = await _TileRepository.AddMapTile(tile); - if (dbTile is not null) - { - return Ok(); - } - else - { - return BadRequest(); - } + return BadRequest(); } + } - [HttpPost] - public async Task UpdateMapTile(MapTile tile) + [HttpPost] + public async Task UpdateMapTile(MapTile tile) + { + MapTile? dbTile = await _TileRepository.UpdateMapTile(tile); + if (dbTile is not null) { - MapTile? dbTile = await _TileRepository.UpdateMapTile(tile); - if (dbTile is not null) - { - return Ok(dbTile); - } - else - { - return NotFound(); - } + return Ok(dbTile); } - - [HttpDelete("{tileID}")] - public async Task DeleteMapTile(int tileID) + else { - MapTile? dbTile = await _TileRepository.DeleteMapTile(tileID); - if (dbTile is not null) - { - return Ok(); - } - else - { - return NotFound(); - } + return NotFound(); } + } - [HttpDelete("{z}/{y}/{x}")] - public async Task DeleteMapTile(int z, int y, int x) + [HttpDelete("{tileID}")] + public async Task DeleteMapTile(int tileID) + { + MapTile? dbTile = await _TileRepository.DeleteMapTile(tileID); + if (dbTile is not null) { - MapTile? tileToDelete = await _TileRepository.GetMapTile(x, y, z); - MapTile? dbTile = await _TileRepository.DeleteMapTile(tileToDelete?.ID ?? -1); - if (dbTile is not null) - { - return Ok(); - } - else - { - return NotFound(); - } + return Ok(); } - - [HttpGet("{z}/{y}/{x}.png")] - public async Task GetMapTileImage(int z, int y, int x) + else { - MapTile? dbTile = await _TileRepository.GetMapTile(x, y, z); - if (dbTile is not null && dbTile.ImageData is not null) - { - return File(dbTile.ImageData, "image/png"); - } - else - { - return NotFound(); - } + return NotFound(); + } + } + [HttpDelete("{z}/{y}/{x}")] + public async Task DeleteMapTile(int z, int y, int x) + { + MapTile? tileToDelete = await _TileRepository.GetMapTile(x, y, z); + MapTile? dbTile = await _TileRepository.DeleteMapTile(tileToDelete?.ID ?? -1); + if (dbTile is not null) + { + return Ok(); } + else + { + return NotFound(); + } + } - [HttpGet("{z}/{y}/{x}")] - public async Task GetMapTile(int z, int y, int x) + [HttpGet("{z}/{y}/{x}.png")] + public async Task GetMapTileImage(int z, int y, int x) + { + MapTile? dbTile = await _TileRepository.GetMapTile(x, y, z); + if (dbTile is not null && dbTile.ImageData is not null) { - MapTile? dbTile = await _TileRepository.GetMapTile(x, y, z); - if (dbTile is not null) - { - return Ok(dbTile); - } - else - { - return NotFound(); - } + return File(dbTile.ImageData, "image/png"); + } + else + { + return NotFound(); + } + } + + [HttpGet("{z}/{y}/{x}")] + public async Task GetMapTile(int z, int y, int x) + { + MapTile? dbTile = await _TileRepository.GetMapTile(x, y, z); + if (dbTile is not null) + { + return Ok(dbTile); } + else + { + return NotFound(); + } + } } \ No newline at end of file diff --git a/Basestation_Software.Api/Data/data.db b/Basestation_Software.Api/Data/data.db index c3cd7da..329fa60 100644 --- a/Basestation_Software.Api/Data/data.db +++ b/Basestation_Software.Api/Data/data.db @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6091d10714744c155f8734b2a67b3b1e1dfbb62e969dcdaa0ac8e6f7d4a9f1c1 -size 2694078464 +oid sha256:b120ae6148fd2f53ade5c46b53c18e0f7b8af48a4e15e5e219e70c2d0141bb29 +size 2694189056 diff --git a/Basestation_Software.Api/Entities/GPSWaypointRepository.cs b/Basestation_Software.Api/Entities/GPSWaypointRepository.cs index 7043d86..1931cb0 100644 --- a/Basestation_Software.Api/Entities/GPSWaypointRepository.cs +++ b/Basestation_Software.Api/Entities/GPSWaypointRepository.cs @@ -1,109 +1,108 @@ -using System.Reflection; using Basestation_Software.Models.Geospatial; using Microsoft.EntityFrameworkCore; +using System.Reflection; -namespace Basestation_Software.Api.Entities +namespace Basestation_Software.Api.Entities; + +public class GPSWaypointRepository : IGPSWaypointRepository { - public class GPSWaypointRepository : IGPSWaypointRepository + // Declare member variables. + private readonly REDDatabase _REDDatabase; + + /// + /// Constructor + /// + /// Implicitly passed in. + public GPSWaypointRepository(REDDatabase db) { - // Declare member variables. - private readonly REDDatabase _REDDatabase; + _REDDatabase = db; + } - /// - /// Constructor - /// - /// Implicitly passed in. - public GPSWaypointRepository(REDDatabase db) - { - _REDDatabase = db; - } + /// + /// Add a GPS waypoint to the database. + /// + /// The new GPS waypoint. + /// The object stored in the DB. + public async Task AddGPSWaypoint(GPSWaypoint waypoint) + { + // Make sure the ID is null. + waypoint.ID = null; + // Add new row to database table. + var result = await _REDDatabase.Waypoints.AddAsync(waypoint); + await _REDDatabase.SaveChangesAsync(); + // Return the inserted value. + return result.Entity; + } - /// - /// Add a GPS waypoint to the database. - /// - /// The new GPS waypoint. - /// The object stored in the DB. - public async Task AddGPSWaypoint(GPSWaypoint waypoint) + /// + /// Remove a GPS waypoint from the database. + /// + /// The ID of the waypoint to remove. + public async Task DeleteGPSWaypoint(int waypointID) + { + // Find the first waypoint with the same ID. + GPSWaypoint? result = await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypointID); + // Check if it was found. + if (result is not null) { - // Make sure the ID is null. - waypoint.ID = null; - // Add new row to database table. - var result = await _REDDatabase.Waypoints.AddAsync(waypoint); + // Remove the row from the database. + _REDDatabase.Waypoints.Remove(result); await _REDDatabase.SaveChangesAsync(); - // Return the inserted value. - return result.Entity; } + return result; + } - /// - /// Remove a GPS waypoint from the database. - /// - /// The ID of the waypoint to remove. - public async Task DeleteGPSWaypoint(int waypointID) - { - // Find the first waypoint with the same ID. - GPSWaypoint? result = await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypointID); - // Check if it was found. - if (result is not null) - { - // Remove the row from the database. - _REDDatabase.Waypoints.Remove(result); - await _REDDatabase.SaveChangesAsync(); - } - return result; - } + /// + /// Get all GPS waypoints in the DB. + /// + /// A list of GPSWaypoint objects. + public async Task> GetAllGPSWaypoints() + { + return await _REDDatabase.Waypoints.ToListAsync(); + } - /// - /// Get all GPS waypoints in the DB. - /// - /// A list of GPSWaypoint objects. - public async Task> GetAllGPSWaypoints() - { - return await _REDDatabase.Waypoints.ToListAsync(); - } + /// + /// Get a waypoint from the DB. + /// + /// The ID of the waypoint to return. + /// A GPSWaypoint object, null if not found. + public async Task GetGPSWaypoint(int waypointID) + { + return await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypointID); + } - /// - /// Get a waypoint from the DB. - /// - /// The ID of the waypoint to return. - /// A GPSWaypoint object, null if not found. - public async Task GetGPSWaypoint(int waypointID) + /// + /// Update the data for a GPSWaypoint in the DB. + /// + /// A GPSWaypoint object containing the new data. + /// The object stored in the database. + public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) + { + // Find the waypoint object to update in the DB. + GPSWaypoint? result = await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypoint.ID); + // Check if we found it. + if (result is not null) { - return await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypointID); - } + // Get the type of the GPSWaypoint class + Type type = typeof(GPSWaypoint); - /// - /// Update the data for a GPSWaypoint in the DB. - /// - /// A GPSWaypoint object containing the new data. - /// The object stored in the database. - public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) - { - // Find the waypoint object to update in the DB. - GPSWaypoint? result = await _REDDatabase.Waypoints.FirstOrDefaultAsync(x => x.ID == waypoint.ID); - // Check if we found it. - if (result is not null) + // Iterate over all properties of the GPSWaypoint class + foreach (PropertyInfo property in type.GetProperties()) { - // Get the type of the GPSWaypoint class - Type type = typeof(GPSWaypoint); + // Get the value of the property from the incoming waypoint object + object? newValue = property.GetValue(waypoint); - // Iterate over all properties of the GPSWaypoint class - foreach (PropertyInfo property in type.GetProperties()) + // If the new value is not null, update the property in the result object + if (newValue != null) { - // Get the value of the property from the incoming waypoint object - object? newValue = property.GetValue(waypoint); - - // If the new value is not null, update the property in the result object - if (newValue != null) - { - property.SetValue(result, newValue); - } + property.SetValue(result, newValue); } - - // Save changes to DB. - await _REDDatabase.SaveChangesAsync(); } - return result; + // Save changes to DB. + await _REDDatabase.SaveChangesAsync(); } + + return result; } } \ No newline at end of file diff --git a/Basestation_Software.Api/Entities/IGPSWaypointRepository.cs b/Basestation_Software.Api/Entities/IGPSWaypointRepository.cs index f5ec80d..191b2d3 100644 --- a/Basestation_Software.Api/Entities/IGPSWaypointRepository.cs +++ b/Basestation_Software.Api/Entities/IGPSWaypointRepository.cs @@ -1,17 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Api.Entities +namespace Basestation_Software.Api.Entities; + +public interface IGPSWaypointRepository { - public interface IGPSWaypointRepository - { - Task AddGPSWaypoint(GPSWaypoint waypoint); - Task DeleteGPSWaypoint(int waypointID); - Task> GetAllGPSWaypoints(); - Task GetGPSWaypoint(int waypointID); - Task UpdateGPSWaypoint(GPSWaypoint waypoint); - } + Task AddGPSWaypoint(GPSWaypoint waypoint); + Task DeleteGPSWaypoint(int waypointID); + Task> GetAllGPSWaypoints(); + Task GetGPSWaypoint(int waypointID); + Task UpdateGPSWaypoint(GPSWaypoint waypoint); } \ No newline at end of file diff --git a/Basestation_Software.Api/Entities/IMapTileRepository.cs b/Basestation_Software.Api/Entities/IMapTileRepository.cs index a537ec1..d667e9c 100644 --- a/Basestation_Software.Api/Entities/IMapTileRepository.cs +++ b/Basestation_Software.Api/Entities/IMapTileRepository.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Api.Entities +namespace Basestation_Software.Api.Entities; + +public interface IMapTileRepository { - public interface IMapTileRepository - { - Task AddMapTile(MapTile tile); - Task UpdateMapTile(MapTile tile); - Task GetMapTile(int x, int y, int z); - Task DeleteMapTile(int tileID); - } + Task AddMapTile(MapTile tile); + Task UpdateMapTile(MapTile tile); + Task GetMapTile(int x, int y, int z); + Task DeleteMapTile(int tileID); } \ No newline at end of file diff --git a/Basestation_Software.Api/Entities/MapTileRepository.cs b/Basestation_Software.Api/Entities/MapTileRepository.cs index f099a71..e8ed511 100644 --- a/Basestation_Software.Api/Entities/MapTileRepository.cs +++ b/Basestation_Software.Api/Entities/MapTileRepository.cs @@ -1,81 +1,80 @@ -using System.Reflection; using Basestation_Software.Models.Geospatial; using Microsoft.EntityFrameworkCore; +using System.Reflection; -namespace Basestation_Software.Api.Entities +namespace Basestation_Software.Api.Entities; + +public class MapTileRepository : IMapTileRepository { - public class MapTileRepository : IMapTileRepository + // Declare member variables. + private readonly REDDatabase _REDDatabase; + + /// + /// Constructor + /// + /// + public MapTileRepository(REDDatabase db) { - // Declare member variables. - private readonly REDDatabase _REDDatabase; + _REDDatabase = db; + } - /// - /// Constructor - /// - /// - public MapTileRepository(REDDatabase db) - { - _REDDatabase = db; - } + public async Task AddMapTile(MapTile tile) + { + // Make sure the ID is null. + tile.ID = null; + // Add a new tile to the database. + var result = await _REDDatabase.MapTiles.AddAsync(tile); + await _REDDatabase.SaveChangesAsync(); + // Return the inserted value. + return result.Entity; + } - public async Task AddMapTile(MapTile tile) + public async Task UpdateMapTile(MapTile tile) + { + // Find the tile object to update in the DB. + MapTile? result = await _REDDatabase.MapTiles.FirstOrDefaultAsync(x => x.ID == tile.ID); + // Check if we found it. + if (result is not null) { - // Make sure the ID is null. - tile.ID = null; - // Add a new tile to the database. - var result = await _REDDatabase.MapTiles.AddAsync(tile); - await _REDDatabase.SaveChangesAsync(); - // Return the inserted value. - return result.Entity; - } + // Get the type of the GPStile class + Type type = typeof(MapTile); - public async Task UpdateMapTile(MapTile tile) - { - // Find the tile object to update in the DB. - MapTile? result = await _REDDatabase.MapTiles.FirstOrDefaultAsync(x => x.ID == tile.ID); - // Check if we found it. - if (result is not null) + // Iterate over all properties of the GPStile class + foreach (PropertyInfo property in type.GetProperties()) { - // Get the type of the GPStile class - Type type = typeof(MapTile); + // Get the value of the property from the incoming tile object + object? newValue = property.GetValue(tile); - // Iterate over all properties of the GPStile class - foreach (PropertyInfo property in type.GetProperties()) + // If the new value is not null, update the property in the result object + if (newValue != null) { - // Get the value of the property from the incoming tile object - object? newValue = property.GetValue(tile); - - // If the new value is not null, update the property in the result object - if (newValue != null) - { - property.SetValue(result, newValue); - } + property.SetValue(result, newValue); } - - // Save changes to DB. - await _REDDatabase.SaveChangesAsync(); } - return result; + // Save changes to DB. + await _REDDatabase.SaveChangesAsync(); } - public async Task DeleteMapTile(int tileID) - { - // Find the first tile with the same ID. - MapTile? result = await _REDDatabase.MapTiles.FirstOrDefaultAsync(x => x.ID == tileID); - // Check if it was found. - if (result is not null) - { - // Remove from the db. - _REDDatabase.MapTiles.Remove(result); - await _REDDatabase.SaveChangesAsync(); - } - return result; - } + return result; + } - public async Task GetMapTile(int x, int y, int z) + public async Task DeleteMapTile(int tileID) + { + // Find the first tile with the same ID. + MapTile? result = await _REDDatabase.MapTiles.FirstOrDefaultAsync(x => x.ID == tileID); + // Check if it was found. + if (result is not null) { - return await _REDDatabase.MapTiles.FirstOrDefaultAsync(tile => tile.X == x && tile.Y == y && tile.Z == z); + // Remove from the db. + _REDDatabase.MapTiles.Remove(result); + await _REDDatabase.SaveChangesAsync(); } + return result; + } + + public async Task GetMapTile(int x, int y, int z) + { + return await _REDDatabase.MapTiles.FirstOrDefaultAsync(tile => tile.X == x && tile.Y == y && tile.Z == z); } } \ No newline at end of file diff --git a/Basestation_Software.Api/Entities/REDDatabase.cs b/Basestation_Software.Api/Entities/REDDatabase.cs index 09fd5ee..84b5392 100644 --- a/Basestation_Software.Api/Entities/REDDatabase.cs +++ b/Basestation_Software.Api/Entities/REDDatabase.cs @@ -2,99 +2,98 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Basestation_Software.Api.Entities +namespace Basestation_Software.Api.Entities; + +public class REDDatabase : DbContext { - public class REDDatabase : DbContext - { - private readonly IConfiguration Configuration; + private readonly IConfiguration Configuration; - /// - /// Constructor - /// - /// The configuration that contains the DB connection string and params. (Implicitly passed in) - public REDDatabase(IConfiguration configuration) - { - // Assign member variables. - Configuration = configuration; - } + /// + /// Constructor + /// + /// The configuration that contains the DB connection string and params. (Implicitly passed in) + public REDDatabase(IConfiguration configuration) + { + // Assign member variables. + Configuration = configuration; + } - /// - /// This is used by the Entity Framework Core to configure the database context. - /// - /// - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseSqlite(Configuration.GetConnectionString("RED_DB")); - } + /// + /// This is used by the Entity Framework Core to configure the database context. + /// + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite(Configuration.GetConnectionString("RED_DB")); + } - /// - /// This is used by the Entity Framework Core and represents a collection of - /// entities in a context. In this case is corresponds to a database table. - /// - /// - public DbSet Waypoints { get; set; } - public DbSet MapTiles { get; set; } + /// + /// This is used by the Entity Framework Core and represents a collection of + /// entities in a context. In this case is corresponds to a database table. + /// + /// + public DbSet Waypoints { get; set; } + public DbSet MapTiles { get; set; } - /// - /// Configure the primary key for the Waypoints table. - /// - /// - public void Configure(EntityTypeBuilder modelBuilder) - { - modelBuilder.HasKey(x => x.ID); - modelBuilder.Property(x => x.ID) - .HasColumnName(@"ID") - //.HasColumnType("int") Weirdly this was upsetting SQLite - .IsRequired() - .ValueGeneratedOnAdd() - ; - } - public void Configure(EntityTypeBuilder modelBuilder) - { - modelBuilder.HasKey(x => x.ID); - modelBuilder.Property(x => x.ID) - .HasColumnName(@"ID") - //.HasColumnType("int") Weirdly this was upsetting SQLite - .IsRequired() - .ValueGeneratedOnAdd() - ; - } + /// + /// Configure the primary key for the Waypoints table. + /// + /// + public void Configure(EntityTypeBuilder modelBuilder) + { + modelBuilder.HasKey(x => x.ID); + modelBuilder.Property(x => x.ID) + .HasColumnName(@"ID") + //.HasColumnType("int") Weirdly this was upsetting SQLite + .IsRequired() + .ValueGeneratedOnAdd() + ; + } + public void Configure(EntityTypeBuilder modelBuilder) + { + modelBuilder.HasKey(x => x.ID); + modelBuilder.Property(x => x.ID) + .HasColumnName(@"ID") + //.HasColumnType("int") Weirdly this was upsetting SQLite + .IsRequired() + .ValueGeneratedOnAdd() + ; + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); - /* - Add some default GPS waypoints to the Waypoint database table. - */ - // SDELC - modelBuilder.Entity().HasData( - new GPSWaypoint - { - ID = 1, - Name = "MDRS", - Latitude = 38.405879, - Longitude = -110.792207, - Altitude = 1280.0, - WaypointColor = System.Drawing.Color.Green.ToArgb(), - SearchRadius = 5.0, - Type = WaypointType.Navigation - } - ); - // MDRS - modelBuilder.Entity().HasData( - new GPSWaypoint - { - ID = 2, - Name = "SDELC", - Latitude = 37.951764, - Longitude = -91.778441, - Altitude = 315.0, - WaypointColor = System.Drawing.Color.Red.ToArgb(), - SearchRadius = 5.0, - Type = WaypointType.Navigation - } - ); - } + /* + Add some default GPS waypoints to the Waypoint database table. + */ + // SDELC + modelBuilder.Entity().HasData( + new GPSWaypoint + { + ID = 1, + Name = "MDRS", + Latitude = 38.405879, + Longitude = -110.792207, + Altitude = 1280.0, + WaypointColor = System.Drawing.Color.Green.ToArgb(), + SearchRadius = 5.0, + Type = WaypointType.Navigation + } + ); + // MDRS + modelBuilder.Entity().HasData( + new GPSWaypoint + { + ID = 2, + Name = "SDELC", + Latitude = 37.951764, + Longitude = -91.778441, + Altitude = 315.0, + WaypointColor = System.Drawing.Color.Red.ToArgb(), + SearchRadius = 5.0, + Type = WaypointType.Navigation + } + ); } } \ No newline at end of file diff --git a/Basestation_Software.Api/Migrations/20240705001141_InitialDBMigration.cs b/Basestation_Software.Api/Migrations/20240705001141_InitialDBMigration.cs index d86439e..d311811 100644 --- a/Basestation_Software.Api/Migrations/20240705001141_InitialDBMigration.cs +++ b/Basestation_Software.Api/Migrations/20240705001141_InitialDBMigration.cs @@ -1,53 +1,50 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional -namespace Basestation_Software.Api.Migrations +namespace Basestation_Software.Api.Migrations; + +/// +public partial class InitialDBMigration : Migration { /// - public partial class InitialDBMigration : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Waypoints", - columns: table => new - { - ID = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - Name = table.Column(type: "TEXT", nullable: true), - Latitude = table.Column(type: "REAL", nullable: true), - Longitude = table.Column(type: "REAL", nullable: true), - Altitude = table.Column(type: "REAL", nullable: true), - Timestamp = table.Column(type: "TEXT", nullable: true), - WaypointColor = table.Column(type: "INTEGER", nullable: true), - SearchRadius = table.Column(type: "REAL", nullable: true), - Type = table.Column(type: "INTEGER", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Waypoints", x => x.ID); - }); + migrationBuilder.CreateTable( + name: "Waypoints", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: true), + Latitude = table.Column(type: "REAL", nullable: true), + Longitude = table.Column(type: "REAL", nullable: true), + Altitude = table.Column(type: "REAL", nullable: true), + Timestamp = table.Column(type: "TEXT", nullable: true), + WaypointColor = table.Column(type: "INTEGER", nullable: true), + SearchRadius = table.Column(type: "REAL", nullable: true), + Type = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Waypoints", x => x.ID); + }); - migrationBuilder.InsertData( - table: "Waypoints", - columns: new[] { "ID", "Altitude", "Latitude", "Longitude", "Name", "SearchRadius", "Timestamp", "Type", "WaypointColor" }, - values: new object[,] - { - { 1, 1280.0, 38.405878999999999, -110.792207, "MDRS", 5.0, new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8159), 0, -16744448 }, - { 2, 315.0, 37.951763999999997, -91.778441000000001, "SDELC", 5.0, new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8289), 0, -65536 } - }); - } + migrationBuilder.InsertData( + table: "Waypoints", + columns: new[] { "ID", "Altitude", "Latitude", "Longitude", "Name", "SearchRadius", "Timestamp", "Type", "WaypointColor" }, + values: new object[,] + { + { 1, 1280.0, 38.405878999999999, -110.792207, "MDRS", 5.0, new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8159), 0, -16744448 }, + { 2, 315.0, 37.951763999999997, -91.778441000000001, "SDELC", 5.0, new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8289), 0, -65536 } + }); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Waypoints"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Waypoints"); } } diff --git a/Basestation_Software.Api/Migrations/20240706231340_Maptiles.cs b/Basestation_Software.Api/Migrations/20240706231340_Maptiles.cs index 84514fa..0c91e5b 100644 --- a/Basestation_Software.Api/Migrations/20240706231340_Maptiles.cs +++ b/Basestation_Software.Api/Migrations/20240706231340_Maptiles.cs @@ -1,66 +1,64 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Basestation_Software.Api.Migrations +namespace Basestation_Software.Api.Migrations; + +/// +public partial class Maptiles : Migration { /// - public partial class Maptiles : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "MapTiles", - columns: table => new - { - ID = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - X = table.Column(type: "INTEGER", nullable: true), - Y = table.Column(type: "INTEGER", nullable: true), - Z = table.Column(type: "INTEGER", nullable: true), - ImageData = table.Column(type: "BLOB", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_MapTiles", x => x.ID); - }); + migrationBuilder.CreateTable( + name: "MapTiles", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + X = table.Column(type: "INTEGER", nullable: true), + Y = table.Column(type: "INTEGER", nullable: true), + Z = table.Column(type: "INTEGER", nullable: true), + ImageData = table.Column(type: "BLOB", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MapTiles", x => x.ID); + }); - migrationBuilder.UpdateData( - table: "Waypoints", - keyColumn: "ID", - keyValue: 1, - column: "Timestamp", - value: new DateTime(2024, 7, 6, 18, 13, 39, 408, DateTimeKind.Local).AddTicks(2836)); + migrationBuilder.UpdateData( + table: "Waypoints", + keyColumn: "ID", + keyValue: 1, + column: "Timestamp", + value: new DateTime(2024, 7, 6, 18, 13, 39, 408, DateTimeKind.Local).AddTicks(2836)); - migrationBuilder.UpdateData( - table: "Waypoints", - keyColumn: "ID", - keyValue: 2, - column: "Timestamp", - value: new DateTime(2024, 7, 6, 18, 13, 39, 408, DateTimeKind.Local).AddTicks(2958)); - } + migrationBuilder.UpdateData( + table: "Waypoints", + keyColumn: "ID", + keyValue: 2, + column: "Timestamp", + value: new DateTime(2024, 7, 6, 18, 13, 39, 408, DateTimeKind.Local).AddTicks(2958)); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "MapTiles"); + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MapTiles"); - migrationBuilder.UpdateData( - table: "Waypoints", - keyColumn: "ID", - keyValue: 1, - column: "Timestamp", - value: new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8159)); + migrationBuilder.UpdateData( + table: "Waypoints", + keyColumn: "ID", + keyValue: 1, + column: "Timestamp", + value: new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8159)); - migrationBuilder.UpdateData( - table: "Waypoints", - keyColumn: "ID", - keyValue: 2, - column: "Timestamp", - value: new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8289)); - } + migrationBuilder.UpdateData( + table: "Waypoints", + keyColumn: "ID", + keyValue: 2, + column: "Timestamp", + value: new DateTime(2024, 7, 4, 19, 11, 41, 140, DateTimeKind.Local).AddTicks(8289)); } } diff --git a/Basestation_Software.Models/Geospatial/GPSWaypoint.cs b/Basestation_Software.Models/Geospatial/GPSWaypoint.cs index b024cb8..79e3524 100644 --- a/Basestation_Software.Models/Geospatial/GPSWaypoint.cs +++ b/Basestation_Software.Models/Geospatial/GPSWaypoint.cs @@ -1,17 +1,14 @@ -using System.Drawing; +namespace Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Models.Geospatial +public class GPSWaypoint { - public class GPSWaypoint - { - public int? ID { get; set; } - public string? Name { get; set; } - public double? Latitude { get; set; } - public double? Longitude { get; set;} - public double? Altitude { get; set; } - public DateTime? Timestamp { get; set; } = DateTime.Now; - public int? WaypointColor { get; set; } - public double? SearchRadius { get; set; } - public WaypointType? Type { get; set; } - } + public int? ID { get; set; } + public string? Name { get; set; } + public double? Latitude { get; set; } + public double? Longitude { get; set; } + public double? Altitude { get; set; } + public DateTime? Timestamp { get; set; } = DateTime.Now; + public int? WaypointColor { get; set; } + public double? SearchRadius { get; set; } + public WaypointType? Type { get; set; } } \ No newline at end of file diff --git a/Basestation_Software.Models/Geospatial/GPSWaypointInput.cs b/Basestation_Software.Models/Geospatial/GPSWaypointInput.cs index c1dcd62..e01496f 100644 --- a/Basestation_Software.Models/Geospatial/GPSWaypointInput.cs +++ b/Basestation_Software.Models/Geospatial/GPSWaypointInput.cs @@ -1,20 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Models.Geospatial +public class GPSWaypointInput { - public class GPSWaypointInput - { - public int ID { get; set; } = -1; - public string Name { get; set; } = ""; - public double Latitude { get; set; } = 0; - public double Longitude { get; set; } = 0; - public double Altitude { get; set; } = 0; - public string Timestamp { get; set; } = DateTime.Now.ToString(); - public string WaypointColor { get; set; } = "rgb(0, 0, 0)"; - public double SearchRadius { get; set; } = 0; - public string Type { get; set; } = "Navigation"; - } + public int ID { get; set; } = -1; + public string Name { get; set; } = ""; + public double Latitude { get; set; } = 0; + public double Longitude { get; set; } = 0; + public double Altitude { get; set; } = 0; + public string Timestamp { get; set; } = DateTime.Now.ToString(); + public string WaypointColor { get; set; } = "rgb(0, 0, 0)"; + public double SearchRadius { get; set; } = 0; + public string Type { get; set; } = "Navigation"; } \ No newline at end of file diff --git a/Basestation_Software.Models/Geospatial/MapTile.cs b/Basestation_Software.Models/Geospatial/MapTile.cs index 09bb911..f43a18a 100644 --- a/Basestation_Software.Models/Geospatial/MapTile.cs +++ b/Basestation_Software.Models/Geospatial/MapTile.cs @@ -1,16 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Models.Geospatial +public class MapTile { - public class MapTile - { - public int? ID { get; set; } - public int? X { get; set; } - public int? Y { get; set; } - public int? Z { get; set; } - public byte[]? ImageData { get; set; } - } + public int? ID { get; set; } + public int? X { get; set; } + public int? Y { get; set; } + public int? Z { get; set; } + public byte[]? ImageData { get; set; } } \ No newline at end of file diff --git a/Basestation_Software.Models/Geospatial/WaypointType.cs b/Basestation_Software.Models/Geospatial/WaypointType.cs index f55d55b..e66f2c2 100644 --- a/Basestation_Software.Models/Geospatial/WaypointType.cs +++ b/Basestation_Software.Models/Geospatial/WaypointType.cs @@ -1,11 +1,10 @@ -namespace Basestation_Software.Models.Geospatial +namespace Basestation_Software.Models.Geospatial; + +public enum WaypointType { - public enum WaypointType - { - Navigation, - Marker, - Object, - Gate, - Obstacle - } + Navigation, + Marker, + Object, + Gate, + Obstacle } \ No newline at end of file diff --git a/Basestation_Software.Models/RoveComm/RoveCommManifest.cs b/Basestation_Software.Models/RoveComm/RoveCommManifest.cs new file mode 100644 index 0000000..33e0bde --- /dev/null +++ b/Basestation_Software.Models/RoveComm/RoveCommManifest.cs @@ -0,0 +1,1035 @@ +namespace Basestation_Software.Models.RoveComm; + +public static class RoveCommConsts +{ + public static readonly int RoveCommVersion = 3; + public static readonly int UDPPort = 11000; + public static readonly int TCPPort = 12000; + public static readonly int HeaderSize = 6; + public static readonly int MaxDataSize = 65535 / 3; + public static readonly int UpdateRate = 100; // milliseconds +} + +public enum RoveCommDataType +{ + INT8_T = 0, + UINT8_T = 1, + INT16_T = 2, + UINT16_T = 3, + INT32_T = 4, + UINT32_T = 5, + FLOAT = 6, + DOUBLE = 7, + CHAR = 8, +} + +public class RoveCommBoardDesc +{ + public string IP { get; init; } + public IReadOnlyDictionary Commands { get; init; } + public IReadOnlyDictionary Telemetry { get; init; } + public IReadOnlyDictionary Errors { get; init; } + + public RoveCommBoardDesc(string ip, + IReadOnlyDictionary? commands = null, + IReadOnlyDictionary? telemetry = null, + IReadOnlyDictionary? errors = null) + { + IP = ip; + Commands = commands ?? new Dictionary(); + Telemetry = telemetry ?? new Dictionary(); + Errors = errors ?? new Dictionary(); + } +} + + +public class RoveCommPacketDesc +{ + public int DataID { get; init; } + public int DataCount { get; init; } + public RoveCommDataType DataType { get; init; } + + public RoveCommPacketDesc(int dataId, int dataCount, RoveCommDataType dataType) + { + DataID = dataId; + DataCount = dataCount; + DataType = dataType; + } +} + +public static class RoveCommManifest +{ + public static class SystemPackets + { + public static readonly int PING = 1; + public static readonly int PING_REPLY = 2; + public static readonly int SUBSCRIBE = 3; + public static readonly int UNSUBSCRIBE = 4; + public static readonly int INVALID_VERSION = 5; + public static readonly int NO_DATA = 6; + } + + public static readonly IReadOnlyDictionary Boards = new Dictionary + { + ["Core"] = new RoveCommBoardDesc + ( + ip: "192.168.2.110", + commands: new Dictionary + { + // [LeftSpeed, RightSpeed] (-1, 1)-> (-100%, 100%) + ["DriveLeftRight"] = new RoveCommPacketDesc + ( + 3000, + 2, + RoveCommDataType.FLOAT + ), + // [LF, LM, LR, RF, RM, RR] (-1, 1)-> (-100%, 100%) + ["DriveIndividual"] = new RoveCommPacketDesc + ( + 3001, + 6, + RoveCommDataType.FLOAT + ), + // [0-override off, 1-override on] + ["WatchdogOverride"] = new RoveCommPacketDesc + ( + 3002, + 1, + RoveCommDataType.UINT8_T + ), + // [Tilt](degrees -180-180) + ["LeftDriveGimbalIncrement"] = new RoveCommPacketDesc + ( + 3003, + 1, + RoveCommDataType.INT16_T + ), + // [Tilt](degrees -180-180) + ["RightDriveGimbalIncrement"] = new RoveCommPacketDesc + ( + 3004, + 1, + RoveCommDataType.INT16_T + ), + // [Pan, Tilt](degrees -180-180) + ["LeftMainGimbalIncrement"] = new RoveCommPacketDesc + ( + 3005, + 2, + RoveCommDataType.INT16_T + ), + // [Pan, Tilt](degrees -180-180) + ["RightMainGimbalIncrement"] = new RoveCommPacketDesc + ( + 3006, + 2, + RoveCommDataType.INT16_T + ), + // [Tilt](degrees -180-180) + ["BackDriveGimbalIncrement"] = new RoveCommPacketDesc + ( + 3007, + 1, + RoveCommDataType.INT16_T + ), + // [R, G, B] (0, 255) + ["LEDRGB"] = new RoveCommPacketDesc + ( + 3008, + 3, + RoveCommDataType.UINT8_T + ), + // [Pattern] (Enum) + ["LEDPatterns"] = new RoveCommPacketDesc + ( + 3009, + 1, + RoveCommDataType.UINT8_T + ), + // [Teleop, Autonomy, Reached Goal] (enum) + ["StateDisplay"] = new RoveCommPacketDesc + ( + 3010, + 1, + RoveCommDataType.UINT8_T + ), + // Set Brightness (0-255) + ["Brightness"] = new RoveCommPacketDesc + ( + 3011, + 1, + RoveCommDataType.UINT8_T + ), + // 0: Teleop, 1: Autonomy + ["SetWatchdogMode"] = new RoveCommPacketDesc + ( + 3012, + 1, + RoveCommDataType.UINT8_T + ), + // Set the message to display on the lighting panel; null terminator ends string early + ["LEDText"] = new RoveCommPacketDesc + ( + 3013, + 256, + RoveCommDataType.CHAR + ) + }, + telemetry: new Dictionary + { + // [LF, LM, LR, RF, RM, RR] (-1, 1)-> (-100%, 100%) + ["DriveSpeeds"] = new RoveCommPacketDesc + ( + 3100, + 6, + RoveCommDataType.FLOAT + ), + // [Roll, Pitch, Yaw] degrees + ["IMUData"] = new RoveCommPacketDesc + ( + 3101, + 3, + RoveCommDataType.FLOAT + ), + // [xAxis, yAxis, zAxis] Accel in m/s^2 + ["AccelerometerData"] = new RoveCommPacketDesc + ( + 3102, + 3, + RoveCommDataType.FLOAT + ) + }, + errors: new Dictionary + { + + } + ), + ["PMS"] = new RoveCommBoardDesc + ( + ip: "192.168.2.102", + commands: new Dictionary + { + // Power off all systems except network (PMS will stay on) + ["EStop"] = new RoveCommPacketDesc + ( + 4000, + 1, + RoveCommDataType.UINT8_T + ), + // Power off all systems including network, cannot recover without physical reboot (PMS will stay on) + ["Suicide"] = new RoveCommPacketDesc + ( + 4001, + 1, + RoveCommDataType.UINT8_T + ), + // Cycle all systems including network off and back on (PMS will stay on) + ["Reboot"] = new RoveCommPacketDesc + ( + 4002, + 1, + RoveCommDataType.UINT8_T + ), + // [Motor, Core, Aux] (bitmasked) [1-Enable, 0-No change] + ["EnableBus"] = new RoveCommPacketDesc + ( + 4003, + 1, + RoveCommDataType.UINT8_T + ), + // [Motor, Core, Aux] (bitmasked) [1-Disable, 0-No change] + ["DisableBus"] = new RoveCommPacketDesc + ( + 4004, + 1, + RoveCommDataType.UINT8_T + ), + // [Motor, Core, Aux] (bitmasked) [1-Enable, 0-Disable] + ["SetBus"] = new RoveCommPacketDesc + ( + 4005, + 1, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // Total current draw from battery + ["PackCurrent"] = new RoveCommPacketDesc + ( + 4100, + 1, + RoveCommDataType.FLOAT + ), + // Pack voltage + ["PackVoltage"] = new RoveCommPacketDesc + ( + 4101, + 1, + RoveCommDataType.FLOAT + ), + // C1, C2, C3, C4, C5, C6 + ["CellVoltage"] = new RoveCommPacketDesc + ( + 4102, + 6, + RoveCommDataType.FLOAT + ), + // Current draw by aux systems (before 12V buck) + ["AuxCurrent"] = new RoveCommPacketDesc + ( + 4103, + 1, + RoveCommDataType.FLOAT + ), + // Current draw from other devices (CS1, CS2, CS3) + ["MiscCurrent"] = new RoveCommPacketDesc + ( + 4104, + 3, + RoveCommDataType.FLOAT + ), + // [Motor, Core, Aux, Network] (bitmasked) [1-Enabled, 0-Disabled] + ["BusStatus"] = new RoveCommPacketDesc + ( + 4105, + 1, + RoveCommDataType.UINT8_T + ) + }, + errors: new Dictionary + { + // Higher current draw than the battery can support. Rover will Reboot automatically + ["PackOvercurrent"] = new RoveCommPacketDesc + ( + 4200, + 1, + RoveCommDataType.UINT8_T + ), + // (bitmasked) [1-Undervolt, 0-OK]. Rover will EStop automatically + ["CellUndervoltage"] = new RoveCommPacketDesc + ( + 4201, + 1, + RoveCommDataType.UINT8_T + ), + // (bitmasked) [1-Critical, 0-OK]. Rover will Suicide automatically + ["CellCritical"] = new RoveCommPacketDesc + ( + 4202, + 1, + RoveCommDataType.UINT8_T + ), + // Aux system current draw too high. Rover will disable Aux bus automatically + ["AuxOvercurrent"] = new RoveCommPacketDesc + ( + 4203, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["Nav"] = new RoveCommBoardDesc + ( + ip: "192.168.2.104", + commands: new Dictionary + { + + }, + telemetry: new Dictionary + { + // [Lat, Long, Alt] [(-90, 90), (-180, 180)(deg), (0, 1000)] + ["GPSLatLonAlt"] = new RoveCommPacketDesc + ( + 6100, + 3, + RoveCommDataType.DOUBLE + ), + // [Pitch, Yaw, Roll] [(-90, 90), (0, 360), (-90, 90)] (deg) + ["IMUData"] = new RoveCommPacketDesc + ( + 6101, + 3, + RoveCommDataType.FLOAT + ), + // [Heading] [ 0, 360 ] + ["CompassData"] = new RoveCommPacketDesc + ( + 6102, + 1, + RoveCommDataType.FLOAT + ), + // [Number of satellites] + ["SatelliteCountData"] = new RoveCommPacketDesc + ( + 6103, + 1, + RoveCommDataType.UINT8_T + ), + // [xAxis, yAxis, zAxis] Accel in m/s^2 + ["AccelerometerData"] = new RoveCommPacketDesc + ( + 6104, + 3, + RoveCommDataType.FLOAT + ), + // [horizontal_accur, vertical_accur, heading_accur, fix_type, is_differentia] [meters, meters, degrees, ublox_navpvt fix type (http://docs.ros.org/en/noetic/api/ublox_msgs/html/msg/NavPVT.html), boolean] + ["AccuracyData"] = new RoveCommPacketDesc + ( + 6105, + 5, + RoveCommDataType.FLOAT + ) + }, + errors: new Dictionary + { + // + ["GPSLockError"] = new RoveCommPacketDesc + ( + 6200, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["BaseStationNav"] = new RoveCommBoardDesc + ( + ip: "192.168.100.112" + ), + ["SignalStack"] = new RoveCommBoardDesc + ( + ip: "192.168.100.101", + commands: new Dictionary + { + // Motor decipercent [-1000, 1000] + ["OpenLoop"] = new RoveCommPacketDesc + ( + 7000, + 1, + RoveCommDataType.INT16_T + ), + // [Heading] [0, 360) + ["SetAngleTarget"] = new RoveCommPacketDesc + ( + 7001, + 1, + RoveCommDataType.FLOAT + ), + // [Rover Lat, Rover Long, Basestation Lat, Basestation Long] [Lat:(-90, 90), Long:(-180, 180)] (deg) + ["SetGPSTarget"] = new RoveCommPacketDesc + ( + 7002, + 4, + RoveCommDataType.DOUBLE + ), + // [0-override off, 1-override on] + ["WatchdogOverride"] = new RoveCommPacketDesc + ( + 7003, + 1, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // [Heading] [0, 360) + ["CompassAngle"] = new RoveCommPacketDesc + ( + 7100, + 1, + RoveCommDataType.FLOAT + ) + }, + errors: new Dictionary + { + // (1-Watchdog timeout, 0-OK) + ["WatchdogStatus"] = new RoveCommPacketDesc + ( + 7200, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["Arm"] = new RoveCommBoardDesc + ( + ip: "192.168.2.107", + commands: new Dictionary + { + // [X, Y1, Y2, Z, P, R] Motor decipercent [-1000, 1000] + ["OpenLoop"] = new RoveCommPacketDesc + ( + 8000, + 6, + RoveCommDataType.INT16_T + ), + // [X, Y1, Y2, Z, P, R] (in, in, in, in, deg, deg) + ["SetPosition"] = new RoveCommPacketDesc + ( + 8001, + 6, + RoveCommDataType.FLOAT + ), + // [X, Y, Z, P, R] (in, in, in, deg, deg, deg) + ["IncrementPosition"] = new RoveCommPacketDesc + ( + 8002, + 5, + RoveCommDataType.FLOAT + ), + // [X, Y, Z, P, R] (in, in, in, deg, deg) + ["SetIK"] = new RoveCommPacketDesc + ( + 8003, + 5, + RoveCommDataType.FLOAT + ), + // [X, Y, Z, P, R] (in, in, in, deg, deg) + ["IncrementIK_RoverRelative"] = new RoveCommPacketDesc + ( + 8004, + 5, + RoveCommDataType.FLOAT + ), + // [X, Y, Z, P, R] (in, in, in, deg, deg) + ["IncrementIK_WristRelative"] = new RoveCommPacketDesc + ( + 8005, + 5, + RoveCommDataType.FLOAT + ), + // [0-disable, 1-enable] + ["Laser"] = new RoveCommPacketDesc + ( + 8006, + 1, + RoveCommDataType.UINT8_T + ), + // [0-retract, 1-extend] + ["Solenoid"] = new RoveCommPacketDesc + ( + 8007, + 1, + RoveCommDataType.UINT8_T + ), + // Motor decipercent [-1000, 1000] + ["Gripper"] = new RoveCommPacketDesc + ( + 8008, + 1, + RoveCommDataType.INT16_T + ), + // [0-override off, 1-override on] + ["WatchdogOverride"] = new RoveCommPacketDesc + ( + 8009, + 1, + RoveCommDataType.UINT8_T + ), + // [X+, X-, Y1+, Y1-, Y2+, Y2-, Z+, Z-, P+, P-] (0-override off, 1-override on) (bitmasked) + ["LimitSwitchOverride"] = new RoveCommPacketDesc + ( + 8010, + 1, + RoveCommDataType.UINT16_T + ), + // [X, Y1, Y2, Z, P, R1, R2] (1-calibrate, 0-no action) (bitmasked) + ["CalibrateEncoder"] = new RoveCommPacketDesc + ( + 8011, + 1, + RoveCommDataType.UINT8_T + ), + // Toggle gripper and roll motors controlled by other packets; 0-Gripper1, 1-Gripper2 + ["SelectGripper"] = new RoveCommPacketDesc + ( + 8012, + 1, + RoveCommDataType.UINT8_T + ), + // [X+, X-, Y1+, Y1-, Y2+, Y2-, Z+, Z-, P+, P-] (0-override off, 1-override on) (bitmasked) + ["SoftLimitOverride"] = new RoveCommPacketDesc + ( + 8013, + 1, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // [X, Y1, Y2, Z, Pitch, Roll1, Roll2] (in, in, in, in, deg, deg, deg) + ["Positions"] = new RoveCommPacketDesc + ( + 8100, + 7, + RoveCommDataType.FLOAT + ), + // [X, Y, Z, P, R] (in, in, in, deg, deg) + ["Coordinates"] = new RoveCommPacketDesc + ( + 8101, + 5, + RoveCommDataType.FLOAT + ), + // [X+, X-, Y1+, Y1-, Y2+, Y2-, Z+, Z-, Pitch+, Pitch-] (0-off, 1-on) (bitmasked) + ["LimitSwitchTriggered"] = new RoveCommPacketDesc + ( + 8102, + 1, + RoveCommDataType.UINT16_T + ) + }, + errors: new Dictionary + { + // (1-Watchdog timeout, 0-OK) + ["WatchdogStatus"] = new RoveCommPacketDesc + ( + 8200, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["ScienceActuation"] = new RoveCommBoardDesc + ( + ip: "192.168.2.108", + commands: new Dictionary + { + // Motor decipercent [-1000, 1000] + ["ScoopAxis_OpenLoop"] = new RoveCommPacketDesc + ( + 9000, + 1, + RoveCommDataType.INT16_T + ), + // Motor decipercent [-1000, 1000] + ["SensorAxis_OpenLoop"] = new RoveCommPacketDesc + ( + 9001, + 1, + RoveCommDataType.INT16_T + ), + // Absolute position (in) + ["ScoopAxis_SetPosition"] = new RoveCommPacketDesc + ( + 9002, + 1, + RoveCommDataType.FLOAT + ), + // Absolute position (in) + ["SensorAxis_SetPosition"] = new RoveCommPacketDesc + ( + 9003, + 1, + RoveCommDataType.FLOAT + ), + // (in) + ["ScoopAxis_IncrementPosition"] = new RoveCommPacketDesc + ( + 9004, + 1, + RoveCommDataType.FLOAT + ), + // (in) + ["SensorAxis_IncrementPosition"] = new RoveCommPacketDesc + ( + 9005, + 1, + RoveCommDataType.FLOAT + ), + // [ScoopAxis+, ScoopAxis-, SensorAxis+, SensorAxis-] (0-override off, 1-override on) (bitmasked) + ["LimitSwitchOverride"] = new RoveCommPacketDesc + ( + 9006, + 1, + RoveCommDataType.UINT8_T + ), + // Motor decipercent [-1000, 1000] + ["Auger"] = new RoveCommPacketDesc + ( + 9007, + 1, + RoveCommDataType.INT16_T + ), + // [0-180] (degrees) + ["Microscope"] = new RoveCommPacketDesc + ( + 9008, + 1, + RoveCommDataType.UINT8_T + ), + // [0-override off, 1-override on] + ["WatchdogOverride"] = new RoveCommPacketDesc + ( + 9010, + 1, + RoveCommDataType.UINT8_T + ), + // [ScoopAxis, SensorAxis, Proboscis] (1-calibrate, 0-no action) (bitmasked) + ["CalibrateEncoder"] = new RoveCommPacketDesc + ( + 9011, + 1, + RoveCommDataType.UINT8_T + ), + // Request the humidity of the instrument + ["RequestHumidity"] = new RoveCommPacketDesc + ( + 9012, + 1, + RoveCommDataType.UINT8_T + ), + // [Pan, Tilt](degrees -180-180) + ["AugerGimbalIncrement"] = new RoveCommPacketDesc + ( + 9013, + 2, + RoveCommDataType.INT16_T + ) + }, + telemetry: new Dictionary + { + // [ScoopAxis, SensorAxis] (in) + ["Positions"] = new RoveCommPacketDesc + ( + 9100, + 2, + RoveCommDataType.FLOAT + ), + // [ScoopAxis+, ScoopAxis-, SensorAxis+, SensorAxis-] (0-off, 1-on) (bitmasked) + ["LimitSwitchTriggered"] = new RoveCommPacketDesc + ( + 9101, + 1, + RoveCommDataType.UINT8_T + ), + // [Humidity] (relative humidity %) + ["Humidity"] = new RoveCommPacketDesc + ( + 9102, + 1, + RoveCommDataType.FLOAT + ), + // (in/s) + ["AugerSpeed"] = new RoveCommPacketDesc + ( + 9103, + 1, + RoveCommDataType.FLOAT + ) + }, + errors: new Dictionary + { + // (1-Watchdog timeout, 0-OK) + ["WatchdogStatus"] = new RoveCommPacketDesc + ( + 9200, + 1, + RoveCommDataType.UINT8_T + ), + // (1-Stalled, 0-OK) + ["AugerStalled"] = new RoveCommPacketDesc + ( + 9201, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["Autonomy"] = new RoveCommBoardDesc + ( + ip: "192.168.3.100", + commands: new Dictionary + { + // + ["StartAutonomy"] = new RoveCommPacketDesc + ( + 11000, + 1, + RoveCommDataType.UINT8_T + ), + // + ["DisableAutonomy"] = new RoveCommPacketDesc + ( + 11001, + 1, + RoveCommDataType.UINT8_T + ), + // [Lat, Lon] + ["AddPositionLeg"] = new RoveCommPacketDesc + ( + 11002, + 2, + RoveCommDataType.DOUBLE + ), + // [Lat, Lon] + ["AddMarkerLeg"] = new RoveCommPacketDesc + ( + 11003, + 2, + RoveCommDataType.DOUBLE + ), + // [Lat, Lon] + ["AddObjectLeg"] = new RoveCommPacketDesc + ( + 11004, + 2, + RoveCommDataType.DOUBLE + ), + // + ["ClearWaypoints"] = new RoveCommPacketDesc + ( + 11005, + 1, + RoveCommDataType.UINT8_T + ), + // A multiplier from 0.0 to 1.0 that will scale the max power effort of Autonomy + ["SetMaxSpeed"] = new RoveCommPacketDesc + ( + 11006, + 1, + RoveCommDataType.FLOAT + ), + // [Enum (AUTONOMYLOG), Enum (AUTONOMYLOG), Enum (AUTONOMYLOG)] {Console, File, RoveComm} + ["SetLoggingLevels"] = new RoveCommPacketDesc + ( + 11007, + 3, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // Enum (AUTONOMYSTATE) + ["CurrentState"] = new RoveCommPacketDesc + ( + 11100, + 1, + RoveCommDataType.UINT8_T + ), + // + ["ReachedGoal"] = new RoveCommPacketDesc + ( + 11101, + 1, + RoveCommDataType.UINT8_T + ), + // String version of most current error log + ["CurrentLog"] = new RoveCommPacketDesc + ( + 11102, + 255, + RoveCommDataType.CHAR + ) + }, + errors: new Dictionary + { + + } + ), + ["Camera1"] = new RoveCommBoardDesc + ( + ip: "192.168.4.100", + commands: new Dictionary + { + // Change which camera a feed is looking at. [0] is the feed, [1] is the camera to view. + ["ChangeCameras"] = new RoveCommPacketDesc + ( + 12000, + 2, + RoveCommDataType.UINT8_T + ), + // Take a picture with the current camera. [0] is the camera to take a picture with. [1] tells the camera whether to restart the stream afterwards. + ["TakePicture"] = new RoveCommPacketDesc + ( + 12001, + 2, + RoveCommDataType.UINT8_T + ), + // Stop the current camera stream. [0] is the camera to stop streaming. [1] is whether to restart the stream. + ["ToggleStream1"] = new RoveCommPacketDesc + ( + 12002, + 2, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // Bitmask values for which cameras are able to stream. LSB is Camera 0, MSB is Camera 7. + ["AvailableCameras"] = new RoveCommPacketDesc + ( + 12100, + 1, + RoveCommDataType.UINT8_T + ), + // Which cameras the system is currently streaming on each port + ["StreamingCameras"] = new RoveCommPacketDesc + ( + 12101, + 4, + RoveCommDataType.UINT8_T + ), + // Picture has been taken. + ["PictureTaken1"] = new RoveCommPacketDesc + ( + 12102, + 1, + RoveCommDataType.UINT8_T + ) + }, + errors: new Dictionary + { + // Camera has errored and stopped streaming. [0] is ID of camera as an integer (not bitmask). + ["CameraUnavailable"] = new RoveCommPacketDesc + ( + 12200, + 1, + RoveCommDataType.UINT8_T + ) + } + ), + ["Camera2"] = new RoveCommBoardDesc + ( + ip: "192.168.4.101", + commands: new Dictionary + { + // Take a picture with the current camera. [0] is the camera to take a picture with. [1] tells the camera whether to restart the stream afterwards. + ["TakePicture"] = new RoveCommPacketDesc + ( + 13001, + 1, + RoveCommDataType.UINT8_T + ), + // Stop the current camera stream. [0] is the camera to stop streaming. [1] is whether to restart the stream. + ["ToggleStream2"] = new RoveCommPacketDesc + ( + 13002, + 2, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // Picture has been taken. + ["PictureTaken2"] = new RoveCommPacketDesc + ( + 13100, + 1, + RoveCommDataType.UINT8_T + ) + }, + errors: new Dictionary + { + + } + ), + ["IRSpectrometer"] = new RoveCommBoardDesc + ( + ip: "192.168.3.104", + commands: new Dictionary + { + + }, + telemetry: new Dictionary + { + + }, + errors: new Dictionary + { + + } + ), + ["Instruments"] = new RoveCommBoardDesc + ( + ip: "192.168.3.105", + commands: new Dictionary + { + // [Green, White] [1-Enabled, 0-Disabled] (bitmasked) + ["EnableLEDs"] = new RoveCommPacketDesc + ( + 16000, + 1, + RoveCommDataType.UINT8_T + ), + // Start a Raman reading, with the provided integration time (milliseconds) + ["RequestRamanReading"] = new RoveCommPacketDesc + ( + 16001, + 1, + RoveCommDataType.UINT32_T + ), + // Start a Reflectance reading, with the provided integration time (milliseconds) + ["RequestReflectanceReading"] = new RoveCommPacketDesc + ( + 16002, + 1, + RoveCommDataType.UINT32_T + ), + // Request the temperature of the instrument + ["RequestTemperature"] = new RoveCommPacketDesc + ( + 16003, + 1, + RoveCommDataType.UINT8_T + ) + }, + telemetry: new Dictionary + { + // Raman CCD elements 1-500 + ["RamanReading_Part1"] = new RoveCommPacketDesc + ( + 16100, + 500, + RoveCommDataType.UINT16_T + ), + // Raman CCD elements 501-1000 + ["RamanReading_Part2"] = new RoveCommPacketDesc + ( + 16101, + 500, + RoveCommDataType.UINT16_T + ), + // Raman CCD elements 1001-1500 + ["RamanReading_Part3"] = new RoveCommPacketDesc + ( + 16102, + 500, + RoveCommDataType.UINT16_T + ), + // Raman CCD elements 1501-2000 + ["RamanReading_Part4"] = new RoveCommPacketDesc + ( + 16103, + 500, + RoveCommDataType.UINT16_T + ), + // Raman CCD elements 2001-2048 + ["RamanReading_Part5"] = new RoveCommPacketDesc + ( + 16104, + 48, + RoveCommDataType.UINT16_T + ), + // Reflectance CCD elements 1-288 + ["ReflectanceReading"] = new RoveCommPacketDesc + ( + 16105, + 288, + RoveCommDataType.UINT8_T + ), + // [Temperature] (degrees C) + ["Temperature"] = new RoveCommPacketDesc + ( + 16106, + 1, + RoveCommDataType.INT8_T + ) + }, + errors: new Dictionary + { + + } + ) + }; +} diff --git a/Basestation_Software.Models/RoveComm/RoveCommPacket.cs b/Basestation_Software.Models/RoveComm/RoveCommPacket.cs new file mode 100644 index 0000000..32b204f --- /dev/null +++ b/Basestation_Software.Models/RoveComm/RoveCommPacket.cs @@ -0,0 +1,45 @@ +namespace Basestation_Software.Models.RoveComm; + +/// +/// RoveComm Packet Format: +/// +/// 0 1 2 3 4 5 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Version | DataID | DataCount | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Data Type | Data (Variable) ... | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// Note: the size of Data is DataCount * DataTypeSize(DataType) +/// +/// One of: sbyte, byte, short, ushort, int, uint, float, double, or char. +public class RoveCommPacket +{ + public int DataID { get; set; } + public int DataCount { get { return Data.Count; } } + public RoveCommDataType DataType { get; init; } + public List Data { get; set; } + + public RoveCommPacket(int dataId, List data) + { + DataID = dataId; + Data = data; + DataType = RoveCommUtils.DataTypeFromType(typeof(T)); + } + + public RoveCommPacket(int dataId, int dataCount) : + this(dataId, new List(new T[dataCount])) + { } + + public RoveCommHeader GetHeader() + { + return new RoveCommHeader + { + Version = (byte)RoveCommConsts.RoveCommVersion, + DataID = (short)DataID, + DataCount = (short)DataCount, + DataType = (byte)DataType, + }; + } +} diff --git a/Basestation_Software.Models/RoveComm/RoveCommUtils.cs b/Basestation_Software.Models/RoveComm/RoveCommUtils.cs new file mode 100644 index 0000000..86a1171 --- /dev/null +++ b/Basestation_Software.Models/RoveComm/RoveCommUtils.cs @@ -0,0 +1,467 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; + +namespace Basestation_Software.Models.RoveComm; + +/// +/// A simple helper struct representing the metadata about a packet as read from the network. +/// +public struct RoveCommHeader +{ + public byte Version; + public short DataID; + public short DataCount; + public byte DataType; +} + +public class RoveCommException : Exception +{ + public RoveCommException(string message) : base(message) { } + public RoveCommException(string message, Exception inner) : base(message, inner) { } +} + +/// +/// A function to be called by RoveComm when a relevant RoveCommPacket is received. +/// +/// The Data field of the received RoveCommPacket. +public delegate Task RoveCommCallback(RoveCommPacket packet); + +/// +/// Helper functions used by RoveComm internally. +/// +public static class RoveCommUtils +{ + /// + /// Convert an integer to a RoveCommDataType safely. + /// + /// The integer to convert. + /// The associated RoveCommDataType. + /// + /// Thrown if the integer does not match a RoveCommDataType. + /// + public static RoveCommDataType ParseDataType(int type) + { + if (Enum.IsDefined(typeof(RoveCommDataType), type)) + { + return (RoveCommDataType)type; + } + else + { + throw new RoveCommException("Failed to convert int to RoveCommDataType."); + } + } + + /// + /// Get the size of a RoveCommDataType. + /// + /// The RoveCommDataType to get the size of. + /// The size in bytes of the RoveCommDataType. + /// + /// Thrown if the RoveCommDataType is unknown. + /// + public static int DataTypeSize(RoveCommDataType type) + { + return type switch + { + RoveCommDataType.INT8_T => 1, + RoveCommDataType.UINT8_T => 1, + RoveCommDataType.INT16_T => 2, + RoveCommDataType.UINT16_T => 2, + RoveCommDataType.INT32_T => 4, + RoveCommDataType.UINT32_T => 4, + RoveCommDataType.FLOAT => 4, + RoveCommDataType.DOUBLE => 8, + RoveCommDataType.CHAR => 1, + _ => throw new RoveCommException("Failed to find size of unknown RoveCommDataType."), // unreachable + }; + } + + /// + /// Convert a System.Type to a RoveCommDataType. + /// + /// The System.Type to convert. + /// The associated RoveCommDataType. + /// + /// Thrown if the System.Type did not match a RoveCommDataType. + /// + public static RoveCommDataType DataTypeFromType(Type type) + { + TypeCode code = Type.GetTypeCode(type); + return code switch + { + TypeCode.SByte => RoveCommDataType.INT8_T, + TypeCode.Byte => RoveCommDataType.UINT8_T, + TypeCode.Int16 => RoveCommDataType.INT16_T, + TypeCode.UInt16 => RoveCommDataType.UINT16_T, + TypeCode.Int32 => RoveCommDataType.INT32_T, + TypeCode.UInt32 => RoveCommDataType.UINT32_T, + TypeCode.Single => RoveCommDataType.FLOAT, + TypeCode.Double => RoveCommDataType.DOUBLE, + TypeCode.Char => RoveCommDataType.CHAR, + _ => throw new RoveCommException("Failed to create RoveCommDataType from unknown System.Type."), + }; + } + + /// + /// Query a board's info form the Manifest by its name. + /// + /// The name of the board. + /// The RoveCommBoardDesc to fill in with informaiton, if the board is found. + /// True if the board was found in the Manifest. + public static bool FindBoardByName(string boardName, out RoveCommBoardDesc? boardDesc) + { + return RoveCommManifest.Boards.TryGetValue(boardName, out boardDesc); + } + + /// + /// Query board and packet info from the Manifest by their names. + /// + /// The name of the board. + /// The name of the packet. + /// The RoveCommBoardDesc to fill in with information, if the board is found. + /// The RoveCommPacketDesc to fill in with information, if the packet is found. + /// True only if both the board and the packet were found in the Manifest. + public static bool FindDataIDByName(string boardName, string packetName, out RoveCommBoardDesc? boardDesc, out RoveCommPacketDesc? packetDesc) + { + packetDesc = null; + return RoveCommManifest.Boards.TryGetValue(boardName, out boardDesc) + && ( + boardDesc.Commands.TryGetValue(packetName, out packetDesc) + || boardDesc.Telemetry.TryGetValue(packetName, out packetDesc) + || boardDesc.Errors.TryGetValue(packetName, out packetDesc) + ); + } + + /// + /// Query board and packet names from the Manifest by its DataID. + /// + /// The DataID to search for. + /// The board name, if the DataID is found. + /// The packet name, if the DataID is found. + /// True only if the DataID was found in the Manifest. + public static bool FindNameByDataID(int dataId, out string? boardName, out string? packetName) + { + boardName = null; + packetName = null; + foreach (var (bname, bdesc) in RoveCommManifest.Boards) + { + foreach (var (pname, pdesc) in bdesc.Commands) + { + if (pdesc.DataID == dataId) { + boardName = bname; + packetName = pname; + return true; + } + } + foreach (var (pname, pdesc) in bdesc.Telemetry) + { + if (pdesc.DataID == dataId) { + boardName = bname; + packetName = pname; + return true; + } + } + foreach (var (pname, pdesc) in bdesc.Errors) + { + if (pdesc.DataID == dataId) { + boardName = bname; + packetName = pname; + return true; + } + } + } + return false; + } + + + /// + /// Read a RoveCommPacket header from the given byte buffer. + /// + /// The byte buffer to read; assumed to be in Big Endian. + /// The parsed RoveCommHeader. + /// + /// Thrown if a RoveCommHeader could not be read from the given buffer. + /// + public static RoveCommHeader ParseHeader(ReadOnlySpan data) + { + if (data.Length < RoveCommConsts.HeaderSize) + { + throw new RoveCommException("Failed to parse RoveCommHeader: not enough bytes."); + } + else + { + if (data[0] != RoveCommConsts.RoveCommVersion) + { + throw new RoveCommException("Failed to parse RoveCommHeader: invalid version."); + } + + return new RoveCommHeader + { + Version = data[0], + DataID = BinaryPrimitives.ReadInt16BigEndian(data.Slice(1, 2)), + DataCount = BinaryPrimitives.ReadInt16BigEndian(data.Slice(3, 2)), + DataType = data[5] + }; + } + } + /// + /// Pack a RoveCommHeader into the given byte buffer. + /// + /// The byte buffer in which to pack the header. + /// The RoveCommHeader to pack. + /// + /// Thrown if the given buffer is too small to hold the header. + /// + public static void PackHeader(Span dest, RoveCommHeader header) + { + if (dest.Length < RoveCommConsts.HeaderSize) + { + throw new RoveCommException("Failed to pack RoveCommHeader: the given buffer is too small for the header."); + } + + dest[0] = (byte)RoveCommConsts.RoveCommVersion; + BinaryPrimitives.WriteInt16BigEndian(dest.Slice(1, 2), header.DataID); + BinaryPrimitives.WriteInt16BigEndian(dest.Slice(3, 2), header.DataCount); + dest[5] = header.DataType; + } + + + /// + /// Read a RoveCommPacket from the given byte buffer. + /// + /// The byte buffer to read; assumed to be in Big Endian. + /// The parsed RoveCommPacket. + /// + /// Thrown if a RoveCommPacket could not be read from the given buffer. + /// + public static RoveCommPacket ParsePacket(ReadOnlySpan data) + { + // Parse header to get metadata -- will error if there isn't enough data to parse the header. + RoveCommHeader header = ParseHeader(data); + if (data[0] != RoveCommConsts.RoveCommVersion) + { + throw new RoveCommException("Failed to parse RoveCommHeader: invalid version."); + } + // Make sure packet data size is not over the maximum size. + int dataSize = DataTypeSize(ParseDataType(header.DataType)) * header.DataCount; + if (dataSize > RoveCommConsts.MaxDataSize) + { + throw new RoveCommException("Failed to parse RoveCommPacket: max packet size exceeded."); + } + // Packet create new packet to write to. + RoveCommPacket packet = new RoveCommPacket(header.DataID, header.DataCount); + + // Create a slice to the data portion of the packet. + var dataBuf = data.Slice(RoveCommConsts.HeaderSize); + // We might have received a packet that isn't as long as it claims to be. + if (dataBuf.Length != dataSize) + { + throw new RoveCommException("Failed to parse RoveCommPacket: not enough data from network to fill packet."); + } + // Read data from the packet from the data buffer. Remember: network byte order is Big Endian! + switch (packet.Data) + { + // SEXY PATTERN MATCHING + case List packetData: + { + var casted = MemoryMarshal.Cast(dataBuf); + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = casted[i]; + } + break; + } + case List packetData: + { + // No cast necessary -- already bytes. + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = dataBuf[i]; + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadInt16BigEndian(dataBuf.Slice(i * 2, 2)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadUInt16BigEndian(dataBuf.Slice(i * 2, 2)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadInt32BigEndian(dataBuf.Slice(i * 4, 4)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadUInt32BigEndian(dataBuf.Slice(i * 4, 4)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadSingleBigEndian(dataBuf.Slice(i * 4, 4)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + packetData[i] = BinaryPrimitives.ReadDoubleBigEndian(dataBuf.Slice(i * 8, 8)); + } + break; + } + case List packetData: + { + for (int i = 0; i < header.DataCount; i++) + { + // Careful: C# stores chars in UTF-16, so they are 2 bytes wide. + // This can cause encoding errors if converting byte[] directly to string. + packetData[i] = (char)dataBuf[i]; + } + break; + } + default: + { + throw new RoveCommException("Failed to parse RoveCommPacket: invalid data type."); + } + } + + return packet; + } + /// + /// Pack a RoveCommPacket into a byte array. + /// + /// The packet to pack. + /// A byte array packed in Big Endian. + /// + /// Thrown if the given packet is too large. + /// + public static byte[] PackPacket(RoveCommPacket packet) + { + int dataSize = packet.DataCount * DataTypeSize(packet.DataType); + if (dataSize > RoveCommConsts.MaxDataSize) + { + throw new RoveCommException("Failed to pack RoveCommPacket: packet exceeds max data size."); + } + byte[] dataBuf = new byte[RoveCommConsts.HeaderSize + dataSize]; + PackPacket(dataBuf, packet); + return dataBuf; + } + public static void PackPacket(Span dest, RoveCommPacket packet) + { + // Make sure the packet and destination buffer are within size constraints. + int packetSize = RoveCommConsts.HeaderSize + packet.DataCount * DataTypeSize(packet.DataType); + if (dest.Length < packetSize) + { + throw new RoveCommException("Failed to pack RoveCommPacket: the given buffer is too small to hold the packet."); + } + if (packetSize > RoveCommConsts.HeaderSize + RoveCommConsts.MaxDataSize) + { + throw new RoveCommException("Failed to pack RoveCommPacket: packet exceeds max data size."); + } + // Pack header in network byte order. + PackHeader(dest, packet.GetHeader()); + // Create a slice to the data portion of the data buffer. + var dataBuf = dest.Slice(RoveCommConsts.HeaderSize); + // Pack data in network byte order. + switch (packet.Data) + { + case List packetData: + { + var casted = MemoryMarshal.Cast(dataBuf); + for (int i = 0; i < packet.DataCount; i++) + { + casted[i] = packetData[i]; + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + dataBuf[0] = packetData[0]; + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteInt16BigEndian(dataBuf.Slice(i * 2, 2), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteUInt16BigEndian(dataBuf.Slice(i * 2, 2), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteInt32BigEndian(dataBuf.Slice(i * 4, 4), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteUInt32BigEndian(dataBuf.Slice(i * 4, 4), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteSingleBigEndian(dataBuf.Slice(i * 4, 4), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + BinaryPrimitives.WriteDoubleBigEndian(dataBuf.Slice(i * 8, 8), packetData[i]); + } + break; + } + case List packetData: + { + for (int i = 0; i < packet.DataCount; i++) + { + dataBuf[i] = (byte)packetData[i]; + } + break; + } + default: + { + throw new RoveCommException("Failed to pack RoveCommPacket: invalid data type."); + } + } + } +} + diff --git a/Basestation_Software.Models/Timers/ITimer.cs b/Basestation_Software.Models/Timers/ITimer.cs index 6c1db89..d738e86 100644 --- a/Basestation_Software.Models/Timers/ITimer.cs +++ b/Basestation_Software.Models/Timers/ITimer.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Basestation_Software.Models.Timers; -namespace Basestation_Software.Models.Timers +public interface ITimer { - public interface ITimer - { - public void Start(int updateIntervalMS); - public void Stop(); - public void Reset(); - } + public void Start(int updateIntervalMS); + public void Stop(); + public void Reset(); } \ No newline at end of file diff --git a/Basestation_Software.Models/Timers/TaskTimer.cs b/Basestation_Software.Models/Timers/TaskTimer.cs index 34779bb..1d67d2e 100644 --- a/Basestation_Software.Models/Timers/TaskTimer.cs +++ b/Basestation_Software.Models/Timers/TaskTimer.cs @@ -1,234 +1,227 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +namespace Basestation_Software.Models.Timers; -namespace Basestation_Software.Models.Timers +public class TaskTimer : ITimer { - public class TaskTimer : ITimer + // Declare member variables. + public Timer? Timer { get; private set; } + public TaskType TaskType { get; private set; } = TaskType.Autonomy; + private bool _startPointChanged = false; + private DateTime _startPoint = DateTime.MinValue; + public DateTime StartPoint { - // Declare member variables. - public Timer? Timer { get; private set; } - public TaskType TaskType { get; private set; } = TaskType.Autonomy; - private bool _startPointChanged = false; - private DateTime _startPoint = DateTime.MinValue; - public DateTime StartPoint + get { return _startPoint; } + set { - get { return _startPoint; } - set - { - _startPoint = value; - _startPointChanged = true; - } + _startPoint = value; + _startPointChanged = true; } - public TimeSpan EndPoint { get; set; } = TimeSpan.MaxValue; - public TimeSpan ElapsedTime { get; private set; } = TimeSpan.Zero; - public Dictionary CheckPoints { get; set; } = new Dictionary(); - public bool IsStarted { get; private set; } = false; - public bool IsPaused { get; private set; } = false; - public bool IsFinished { get; private set; } = false; - public bool IsBeingConfigured { get; private set; } = false; - - // Delegates and events. - public delegate Task TimerTickCallback(TaskType timerTaskType, TimeSpan elapsedTime); - private event TimerTickCallback? TimerTickNotifier; - - /// - /// Constructor. - /// - /// - /// - public TaskTimer(TimerTickCallback callback, TaskType timerType) + } + public TimeSpan EndPoint { get; set; } = TimeSpan.MaxValue; + public TimeSpan ElapsedTime { get; private set; } = TimeSpan.Zero; + public Dictionary CheckPoints { get; set; } = []; + public bool IsStarted { get; private set; } = false; + public bool IsPaused { get; private set; } = false; + public bool IsFinished { get; private set; } = false; + public bool IsBeingConfigured { get; private set; } = false; + + // Delegates and events. + public delegate Task TimerTickCallback(TaskType timerTaskType, TimeSpan elapsedTime); + private event TimerTickCallback? TimerTickNotifier; + + /// + /// Constructor. + /// + /// + /// + public TaskTimer(TimerTickCallback callback, TaskType timerType) + { + // Assigm member variables. + TimerTickNotifier += callback; + TaskType = timerType; + } + + /// + /// Start the timer. + /// + /// The interval in ms that the timer metrics and callbacks will be updated/invoked. + public void Start(int updateIntervalMS = 100) + { + if (IsFinished) { - // Assigm member variables. - TimerTickNotifier += callback; - TaskType = timerType; + return; } - - /// - /// Start the timer. - /// - /// The interval in ms that the timer metrics and callbacks will be updated/invoked. - public void Start(int updateIntervalMS = 100) + if (!IsPaused) { - if (IsFinished) - { - return; - } - if (!IsPaused) - { - StartPoint = DateTime.Now; - // Create new timer. - Timer = new Timer(UpdateElapsedTime, null, 0, updateIntervalMS); - } - else - { - // Offest the start point by the elapsed time. - StartPoint = DateTime.Now - ElapsedTime; - } - IsStarted = true; - IsPaused = false; - IsFinished = false; + StartPoint = DateTime.Now; + // Create new timer. + Timer = new Timer(UpdateElapsedTime, null, 0, updateIntervalMS); } - - /// - /// Pause the timer. - /// - public void Stop() + else { - if (!IsStarted || IsFinished) - { - return; - } - IsStarted = true; - IsPaused = true; - IsFinished = false; + // Offest the start point by the elapsed time. + StartPoint = DateTime.Now - ElapsedTime; } + IsStarted = true; + IsPaused = false; + IsFinished = false; + } - /// - /// Reset the timer back to its initial state. - /// - public void Reset() + /// + /// Pause the timer. + /// + public void Stop() + { + if (!IsStarted || IsFinished) { - Timer?.Change(Timeout.Infinite, Timeout.Infinite); - IsStarted = false; - IsPaused = false; - IsFinished = false; - StartPoint = DateTime.Now; - ElapsedTime = TimeSpan.Zero; - UpdateElapsedTime(null); + return; } + IsStarted = true; + IsPaused = true; + IsFinished = false; + } - /// - /// Given a checkpoint name, skip the timer to that checkpoint. - /// - /// The name of the checkpoint to skip to. - public void SkipToCheckPoint(string checkPointName) + /// + /// Reset the timer back to its initial state. + /// + public void Reset() + { + Timer?.Change(Timeout.Infinite, Timeout.Infinite); + IsStarted = false; + IsPaused = false; + IsFinished = false; + StartPoint = DateTime.Now; + ElapsedTime = TimeSpan.Zero; + UpdateElapsedTime(null); + } + + /// + /// Given a checkpoint name, skip the timer to that checkpoint. + /// + /// The name of the checkpoint to skip to. + public void SkipToCheckPoint(string checkPointName) + { + if (CheckPoints.ContainsKey(checkPointName)) { - if (CheckPoints.ContainsKey(checkPointName)) - { - // Sum all the checkpoints up to the checkpoint we want to skip to. - StartPoint = DateTime.Now - TimeSpan.FromSeconds(CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPointName) + 1).Sum(x => x.Value.TotalSeconds)); - UpdateElapsedTime(null); - } + // Sum all the checkpoints up to the checkpoint we want to skip to. + StartPoint = DateTime.Now - TimeSpan.FromSeconds(CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPointName) + 1).Sum(x => x.Value.TotalSeconds)); + UpdateElapsedTime(null); } + } - /// - /// Skip to the next checkpoint. - /// - public void SkipToNextCheckPoint() + /// + /// Skip to the next checkpoint. + /// + public void SkipToNextCheckPoint() + { + if (CheckPoints.Count > 0) { - if (CheckPoints.Count > 0) + // Figure out which checkpoints we are between. Checkpoints should be added in order since we are using a dictionary that just stores the duration of each checkpoint. + foreach (var checkPoint in CheckPoints) { - // Figure out which checkpoints we are between. Checkpoints should be added in order since we are using a dictionary that just stores the duration of each checkpoint. - foreach (var checkPoint in CheckPoints) + // Sum the checkpoints before with this one. + double checkPointStart = CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPoint.Key) + 1).Sum(x => x.Value.TotalSeconds); + // Check if the elapsed time is between the current checkpoint and the next checkpoint. + if (checkPointStart >= ElapsedTime.TotalSeconds) { - // Sum the checkpoints before with this one. - double checkPointStart = CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPoint.Key) + 1).Sum(x => x.Value.TotalSeconds); - // Check if the elapsed time is between the current checkpoint and the next checkpoint. - if (checkPointStart >= ElapsedTime.TotalSeconds) - { - SkipToCheckPoint(checkPoint.Key); - break; - } + SkipToCheckPoint(checkPoint.Key); + break; } } } + } - /// - /// Go back to the previous checkpoint. - /// - public void SkipToPreviousCheckPoint() + /// + /// Go back to the previous checkpoint. + /// + public void SkipToPreviousCheckPoint() + { + if (CheckPoints.Count > 0) { - if (CheckPoints.Count > 0) + // Figure out which checkpoints we are between. Checkpoints should be added in order since we are using a dictionary that just stores the duration of each checkpoint. + foreach (var checkPoint in CheckPoints) { - // Figure out which checkpoints we are between. Checkpoints should be added in order since we are using a dictionary that just stores the duration of each checkpoint. - foreach (var checkPoint in CheckPoints) + // Sum the checkpoints before with this one. + double checkPointStart = CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPoint.Key) + 1).Sum(x => x.Value.TotalSeconds); + // Check if the elapsed time is between the current checkpoint and the next checkpoint. + if (checkPointStart <= ElapsedTime.TotalSeconds) { - // Sum the checkpoints before with this one. - double checkPointStart = CheckPoints.Take(CheckPoints.Keys.ToList().IndexOf(checkPoint.Key) + 1).Sum(x => x.Value.TotalSeconds); - // Check if the elapsed time is between the current checkpoint and the next checkpoint. - if (checkPointStart <= ElapsedTime.TotalSeconds) + // Check if the checkpoint start is zero. This means we haven't reached the first checkpoint yet. + if (checkPointStart == 0) { - // Check if the checkpoint start is zero. This means we haven't reached the first checkpoint yet. - if (checkPointStart == 0) - { - StartPoint = DateTime.Now; - UpdateElapsedTime(null); - break; - } - else - { - SkipToCheckPoint(checkPoint.Key); - break; - } + StartPoint = DateTime.Now; + UpdateElapsedTime(null); + break; } else { - // If we are at the first checkpoint, reset the timer. - Reset(); + SkipToCheckPoint(checkPoint.Key); break; } } - } - } - - /// - /// Updates timer metrics. - /// - /// - /// - private async void UpdateElapsedTime(object? state) - { - // Check if the timer is paused, but if the start time was changed, we need to update the elapsed time. - if (!IsPaused || _startPointChanged) - { - // Check if the timer is finished. - if (DateTime.Now - StartPoint >= EndPoint) + else { - Timer?.Change(Timeout.Infinite, Timeout.Infinite); - IsFinished = true; + // If we are at the first checkpoint, reset the timer. + Reset(); + break; } - ElapsedTime = DateTime.Now - StartPoint; - _startPointChanged = false; - } - if (TimerTickNotifier is not null) - { - await TimerTickNotifier.Invoke(TaskType, ElapsedTime); } } + } - /// - /// Converts a timespan into a short readable string. For example, 1h 30m 20s. - /// - /// The timespan to parse into a short string. - /// - public static string TimeSpanToShortReadableString(TimeSpan timeSpan) + /// + /// Updates timer metrics. + /// + /// + /// + private async void UpdateElapsedTime(object? state) + { + // Check if the timer is paused, but if the start time was changed, we need to update the elapsed time. + if (!IsPaused || _startPointChanged) { - if (timeSpan.TotalSeconds < 1) + // Check if the timer is finished. + if (DateTime.Now - StartPoint >= EndPoint) { - return "0s"; + Timer?.Change(Timeout.Infinite, Timeout.Infinite); + IsFinished = true; } + ElapsedTime = DateTime.Now - StartPoint; + _startPointChanged = false; + } + if (TimerTickNotifier is not null) + { + await TimerTickNotifier.Invoke(TaskType, ElapsedTime); + } + } - var parts = new List(); + /// + /// Converts a timespan into a short readable string. For example, 1h 30m 20s. + /// + /// The timespan to parse into a short string. + /// + public static string TimeSpanToShortReadableString(TimeSpan timeSpan) + { + if (timeSpan.TotalSeconds < 1) + { + return "0s"; + } - if (timeSpan.Hours > 0) - { - parts.Add($"{timeSpan.Hours}h"); - } + var parts = new List(); - if (timeSpan.Minutes > 0) - { - parts.Add($"{timeSpan.Minutes}m"); - } + if (timeSpan.Hours > 0) + { + parts.Add($"{timeSpan.Hours}h"); + } - if (timeSpan.Seconds > 0) - { - parts.Add($"{timeSpan.Seconds}s"); - } + if (timeSpan.Minutes > 0) + { + parts.Add($"{timeSpan.Minutes}m"); + } - return string.Join(" ", parts); + if (timeSpan.Seconds > 0) + { + parts.Add($"{timeSpan.Seconds}s"); } - } + return string.Join(" ", parts); + } } \ No newline at end of file diff --git a/Basestation_Software.Models/Timers/TimerType.cs b/Basestation_Software.Models/Timers/TimerType.cs index 55f2c80..8a7a762 100644 --- a/Basestation_Software.Models/Timers/TimerType.cs +++ b/Basestation_Software.Models/Timers/TimerType.cs @@ -1,10 +1,9 @@ -namespace Basestation_Software.Models.Timers +namespace Basestation_Software.Models.Timers; + +public enum TaskType { - public enum TaskType - { - Autonomy, - Science, - ExtremeDelivery, - EquipmentServicing - } + Autonomy, + Science, + ExtremeDelivery, + EquipmentServicing } \ No newline at end of file diff --git a/Basestation_Software.Web/Core/App.razor b/Basestation_Software.Web/Core/App.razor index ce8f82c..af069a1 100644 --- a/Basestation_Software.Web/Core/App.razor +++ b/Basestation_Software.Web/Core/App.razor @@ -5,18 +5,22 @@ + + - - - + + + + + diff --git a/Basestation_Software.Web/Core/Components/PingGraph.razor b/Basestation_Software.Web/Core/Components/PingGraph.razor index 8d07fe8..cd9914f 100644 --- a/Basestation_Software.Web/Core/Components/PingGraph.razor +++ b/Basestation_Software.Web/Core/Components/PingGraph.razor @@ -10,7 +10,7 @@
-
Ping Graph
+

Ping Graph

@if (!_dataLoaded) diff --git a/Basestation_Software.Web/Core/Components/RoveCommExample.razor b/Basestation_Software.Web/Core/Components/RoveCommExample.razor new file mode 100644 index 0000000..47f8b39 --- /dev/null +++ b/Basestation_Software.Web/Core/Components/RoveCommExample.razor @@ -0,0 +1,123 @@ +@implements IAsyncDisposable +@inject RoveCommService _RoveCommService +@using Basestation_Software.Models.RoveComm + +
+
+
RoveComm Test
+
+
+
+
+ + +
+ + +
+

@_message

+ @if (!_dataLoaded) + { +
+
+
+

Waiting for packets...

+
+
+ } + else + { +
    + @foreach(var message in _messages) + { +
  • @message
  • + } +
+ } +
+
+
+ + +@code +{ + private bool _dataLoaded = false; + private string? _message; + private bool _isDisabled; + private readonly List _messages = []; + + protected override async Task OnInitializedAsync() + { + // Subscribe to all DataIDs for each type. + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + _RoveCommService.On(0, AddMessage); + + _RoveCommService.On("Core", "DriveLeftRight", async (packet) => + { + _messages.Add("Received DriveLeftRight packet."); + _dataLoaded = true; + await InvokeAsync(StateHasChanged); + }); + + await InvokeAsync(StateHasChanged); + } + + public async Task AddMessage(RoveCommPacket packet) + { + if (RoveCommUtils.FindNameByDataID(packet.DataID, out string? boardName, out string? packetName)) + { + _messages.Add($"Received {packetName} from {boardName}. Data: {string.Join(", ", packet.Data)}"); + } + else + { + _messages.Add($"Received Packet with unknown DataID {packet.DataID}. Data: {string.Join(", ", packet.Data)}"); + } + _dataLoaded = true; + await InvokeAsync(StateHasChanged); + } + + public async Task SendPacketTCP() + { + await _RoveCommService.SendAsync(6969, [1, 2, 3], "127.0.0.1", reliable: true); + } + public async Task SendPacketUDP() + { + await _RoveCommService.SendAsync(6969, [1, 2, 3], "127.0.0.1", reliable: false); + } + public async Task ListenForPacket() + { + _message = "Waiting..."; + _isDisabled = true; + var result = await _RoveCommService.Listen("Core", "DriveLeftRight", 3000); + if (result is not null) + { + _message = "Received!"; + } + else + { + _message = "Timed out."; + } + _isDisabled = false; + } + + public async Task SendManifestPacket() + { + await _RoveCommService.SendAsync("Core", "DriveLeftRight", [5.5f, 5.5f]); + // For testing, also send a copy to the loopback address, since RoveComm will send the packet to 192.168.2.110. + RoveCommUtils.FindDataIDByName("Core", "DriveLeftRight", out var board, out var packet); + await _RoveCommService.SendAsync(packet!.DataID, [5.5f, 5.5f], "127.0.0.1"); + } + + public async ValueTask DisposeAsync() + { + _RoveCommService.Clear(AddMessage); + await Task.CompletedTask; + } +} diff --git a/Basestation_Software.Web/Core/Components/RoverMap.razor b/Basestation_Software.Web/Core/Components/RoverMap.razor index 5572e72..118df3e 100644 --- a/Basestation_Software.Web/Core/Components/RoverMap.razor +++ b/Basestation_Software.Web/Core/Components/RoverMap.razor @@ -2,6 +2,65 @@ @inject MapTileService _MapTileService @inject GPSWaypointService _GPSWaypointService @inject NavigationManager _NavigationManager +@inject IJSRuntime _IJSRuntime + + -
+
-
Map
+

Map

@if (_cachingMapTiles) {
@@ -35,63 +94,36 @@ } else { - +
}
+
@code { + // Component parameters. + [Parameter] + public string ID { get; set; } = ""; + [Parameter] + public double InitialLat { get; set; } = 40; + + [Parameter] + public double InitialLong { get; set; } = -100; + + [Parameter] + public int InitialZoomLevel { get; set; } = 2; + // Declare page member variables. + private string jsInteropName; // Flags. private bool _dataLoaded = false; private bool _cachingMapTiles = false; - // Map configuration parameters. - private RealTimeMap.Basemap basemaps; - private RealTimeMap.LoadParameters parameters; - protected override void OnInitialized() { - // Get base URI for the API endpoint. - Uri baseUri = new Uri(_NavigationManager.BaseUri); - string baseUriWithoutPort = $"{baseUri.Scheme}://{baseUri.Host}"; - - - // Add layers to the map, use openmaps by default. - basemaps = new RealTimeMap.Basemap() - { - basemap_layers = new List - { - new RealTimeMap.BasemapConfigLayer() - { - url = baseUriWithoutPort + ":5000/api/MapTiles/{z}/{y}/{x}.png", - attribution = "Basestation_Software.Api", - title = "AutoCache API Storage", - max_zoom = 20 - } - } - }; - - // Construct the parameters object. - parameters = new RealTimeMap.LoadParameters() - { - // Set the default location when the map is initialized. - location = new RealTimeMap.Location() - { - latitude = 37.951764, - longitude = -91.778441, - }, - zoom_level = 18, - basemap = basemaps, - map_scale = new RealTimeMap.MapScale() - { - has = true, - meters = true, - miles = false - } - }; + this.jsInteropName = $"window.roverMaps.{ID}"; } @@ -102,42 +134,89 @@ await InvokeAsync(StateHasChanged); } - void IDisposable.Dispose() + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) + { + // Create leaflet map. + Uri baseUri = new Uri(_NavigationManager.BaseUri); + await _IJSRuntime.InvokeVoidAsync($"{jsInteropName}.init", [DotNetObjectReference.Create(this), $"{baseUri.Scheme}://{baseUri.Host}:5000/api/MapTiles/{{z}}/{{y}}/{{x}}.png"]); + // Plot waypoints and keep them updated. + await PlotWaypoints(); + _GPSWaypointService.SubscribeToWaypointsChanges(OnWaypointSync); + } } - public async Task OnZoomLevel(RealTimeMap.MapZoomEventArgs args) + private async Task OnWaypointSync() { - #if DEBUG - using (HttpClient client = new HttpClient()) - { - client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); - client.DefaultRequestHeaders.Referrer = new Uri("http://mt1.google.com"); + // Update waypoint markers. + await _IJSRuntime.InvokeVoidAsync($"{jsInteropName}.clearWaypointMarkers"); + await PlotWaypoints(); + } - var tasks = new List(); + private async Task PlotWaypoints() + { + await Task.WhenAll(_GPSWaypointService.GetGPSWaypoints().ConvertAll(waypoint => + { + return _IJSRuntime.InvokeVoidAsync($"{jsInteropName}.addWaypointMarker", + [ + waypoint.Latitude.GetValueOrDefault(0), + waypoint.Longitude.GetValueOrDefault(0), + waypoint.SearchRadius.GetValueOrDefault(0), + ColorTranslator.ToHtml(Color.FromArgb(waypoint.WaypointColor.GetValueOrDefault(0))) + ]).AsTask(); + })); + } - // Convert geographic bounds to tile coordinates - int tileX = LonToTileX(args.centerOfView.longitude, args.zoomLevel); - int tileY = LatToTileY(args.centerOfView.latitude, args.zoomLevel); - - for (int y = (tileY - 3); y < (tileY + 3); y++) + [JSInvokable] + public async Task AddWaypoint(double lat, double lon) + { + await _GPSWaypointService.AddGPSWaypoint(new GPSWaypoint { - for (int x = (tileX - 3); x < (tileX + 3); x++) - { - MapTile? mapTile = await _MapTileService.GetMapTile(x, y, args.zoomLevel); - if (mapTile is null) - { - string url = $"http://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={args.zoomLevel}"; - tasks.Add(DownloadAndSaveTile(client, url, x, y, args.zoomLevel)); - _cachingMapTiles = true; - } - } - } + ID = -1, + Name = "", + Latitude = lat, + Longitude = lon, + Altitude = 0, + WaypointColor = 0, + SearchRadius = 0, + Type = WaypointType.Navigation, + }); + } + + [JSInvokable] + public async Task OnZoomLevel(double lat, double lon, int zoom) + { +#if DEBUG + using (HttpClient client = new HttpClient()) + { + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"); + client.DefaultRequestHeaders.Referrer = new Uri("http://mt1.google.com"); + + var tasks = new List(); - await Task.WhenAll(tasks); - _cachingMapTiles = false; + // Convert geographic bounds to tile coordinates + int tileX = LonToTileX(lon, zoom); + int tileY = LatToTileY(lat, zoom); + + for (int y = (tileY - 3); y < (tileY + 3); y++) + { + for (int x = (tileX - 3); x < (tileX + 3); x++) + { + MapTile? mapTile = await _MapTileService.GetMapTile(x, y, zoom); + if (mapTile is null) + { + string url = $"http://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={zoom}"; + tasks.Add(DownloadAndSaveTile(client, url, x, y, zoom)); + _cachingMapTiles = true; } + } + } + + await Task.WhenAll(tasks); + _cachingMapTiles = false; + } #endif } @@ -171,4 +250,6 @@ double latRad = lat * Math.PI / 180.0; return (int)Math.Floor((1.0 - Math.Log(Math.Tan(latRad) + 1.0 / Math.Cos(latRad)) / Math.PI) / 2.0 * Math.Pow(2.0, zoom)); } + + void IDisposable.Dispose() { } } diff --git a/Basestation_Software.Web/Core/Components/TaskTimers.razor b/Basestation_Software.Web/Core/Components/TaskTimers.razor index 4f9788d..42d311f 100644 --- a/Basestation_Software.Web/Core/Components/TaskTimers.razor +++ b/Basestation_Software.Web/Core/Components/TaskTimers.razor @@ -33,9 +33,7 @@
-
-

Task Timer

-
+

Task Timer

diff --git a/Basestation_Software.Web/Core/Components/Waypoints.razor b/Basestation_Software.Web/Core/Components/Waypoints.razor index eb83917..bec7c5b 100644 --- a/Basestation_Software.Web/Core/Components/Waypoints.razor +++ b/Basestation_Software.Web/Core/Components/Waypoints.razor @@ -5,14 +5,14 @@
-
Waypoints
-
 
- +

Waypoints

+
 
+
@if (_waypointsToUpdate.Count > 0 || _waypointsToInsert.Count > 0) { -
 
+
 
} diff --git a/Basestation_Software.Web/Core/Pages/RED.razor b/Basestation_Software.Web/Core/Pages/RED.razor index 1b781e8..89d0625 100644 --- a/Basestation_Software.Web/Core/Pages/RED.razor +++ b/Basestation_Software.Web/Core/Pages/RED.razor @@ -10,7 +10,7 @@
-
+
@@ -27,10 +27,10 @@
-
+
- +
diff --git a/Basestation_Software.Web/Core/Pages/RON.razor b/Basestation_Software.Web/Core/Pages/RON.razor index df23929..224b3b5 100644 --- a/Basestation_Software.Web/Core/Pages/RON.razor +++ b/Basestation_Software.Web/Core/Pages/RON.razor @@ -5,6 +5,9 @@
+
+ +
@code diff --git a/Basestation_Software.Web/Core/Services/CookieService.cs b/Basestation_Software.Web/Core/Services/CookieService.cs index 5576bc9..f73967b 100644 --- a/Basestation_Software.Web/Core/Services/CookieService.cs +++ b/Basestation_Software.Web/Core/Services/CookieService.cs @@ -1,79 +1,83 @@ using Microsoft.JSInterop; -namespace Basestation_Software.Web.Core.Services +namespace Basestation_Software.Web.Core.Services; + +public class CookieService { - public class CookieService + // Declare member variables. + private readonly IJSRuntime _JSRuntime; + string expires = ""; + + /// + /// Constructor + /// + /// Implicitly passed in. + public CookieService(IJSRuntime jsRuntime) { - // Declare member variables. - private readonly IJSRuntime _JSRuntime; - string expires = ""; + _JSRuntime = jsRuntime; + ExpireDays = 30; + } - /// - /// Constructor - /// - /// Implicitly passed in. - public CookieService(IJSRuntime jsRuntime) - { - _JSRuntime = jsRuntime; - ExpireDays = 30; - } + /// + /// Stores a cookie in the browser. + /// + /// The key to reference the cookie by later. + /// The calue to store in the cookie + /// The number of days before the cookie expires. + /// + public async Task SetValue(string key, string value, int? days = null) + { + var curExp = (days != null) ? (days > 0 ? DateToUTC(days.Value) : "") : expires; + await SetCookie($"{key}={value}; expires={curExp}; path=/"); + } - /// - /// Stores a cookie in the browser. - /// - /// The key to reference the cookie by later. - /// The calue to store in the cookie - /// The number of days before the cookie expires. - /// - public async Task SetValue(string key, string value, int? days = null) - { - var curExp = (days != null) ? (days > 0 ? DateToUTC(days.Value) : "") : expires; - await SetCookie($"{key}={value}; expires={curExp}; path=/"); - } + /// + /// Gets a cookie from the browser. + /// + /// The name of the cookie. + /// The default value if the cookie isn't found. + /// + public async Task GetValue(string key, string def = "") + { + var cValue = await GetCookie(); + if (string.IsNullOrEmpty(cValue)) return def; - /// - /// Gets a cookie from the browser. - /// - /// The name of the cookie. - /// The default value if the cookie isn't found. - /// - public async Task GetValue(string key, string def = "") + var vals = cValue.Split(';'); + foreach (var val in vals) { - var cValue = await GetCookie(); - if (string.IsNullOrEmpty(cValue)) return def; - - var vals = cValue.Split(';'); - foreach (var val in vals) - if(!string.IsNullOrEmpty(val) && val.IndexOf('=') > 0) - if(val.Substring(0, val.IndexOf('=')).Trim().Equals(key, StringComparison.OrdinalIgnoreCase)) - return val.Substring(val.IndexOf('=') + 1); - return def; + if (!string.IsNullOrEmpty(val) && val.IndexOf('=') > 0) + { + if (val.Substring(0, val.IndexOf('=')).Trim().Equals(key, StringComparison.OrdinalIgnoreCase)) + return val.Substring(val.IndexOf('=') + 1); + } } - /// - /// Sets a cookie. - /// - /// Cookie info. - /// - private async Task SetCookie(string value) - { - await _JSRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{value}\""); - } + return def; + } - /// - /// Gets a cookie. - /// - /// The cookie info. - private async Task GetCookie() - { - return await _JSRuntime.InvokeAsync("eval", $"document.cookie"); - } + /// + /// Sets a cookie. + /// + /// Cookie info. + /// + private async Task SetCookie(string value) + { + await _JSRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{value}\""); + } - public int ExpireDays - { - set => expires = DateToUTC(value); - } + /// + /// Gets a cookie. + /// + /// The cookie info. + private async Task GetCookie() + { + return await _JSRuntime.InvokeAsync("eval", $"document.cookie"); + } - private static string DateToUTC(int days) => DateTime.Now.AddDays(days).ToUniversalTime().ToString("R"); + public int ExpireDays + { + set => expires = DateToUTC(value); } + + private static string DateToUTC(int days) => DateTime.Now.AddDays(days).ToUniversalTime().ToString("R"); } \ No newline at end of file diff --git a/Basestation_Software.Web/Core/Services/GPSWaypointService.cs b/Basestation_Software.Web/Core/Services/GPSWaypointService.cs index 0bb1f09..ef00bde 100644 --- a/Basestation_Software.Web/Core/Services/GPSWaypointService.cs +++ b/Basestation_Software.Web/Core/Services/GPSWaypointService.cs @@ -1,119 +1,118 @@ using Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Web.Core.Services +namespace Basestation_Software.Web.Core.Services; + +public class GPSWaypointService { - public class GPSWaypointService - { - // Injected services. - private readonly HttpClient _HttpClient; - // Declare member variables. - private List _gpsWaypoints = new List(); + // Injected services. + private readonly HttpClient _HttpClient; + // Declare member variables. + private List _gpsWaypoints = []; - // Method delegates and events. - public delegate Task SyncWaypointsCallback(); - private event SyncWaypointsCallback? SyncWaypointsNotifier; + // Method delegates and events. + public delegate Task SyncWaypointsCallback(); + private event SyncWaypointsCallback? SyncWaypointsNotifier; - /// - /// Constructor - /// - /// Implicitly passed in, used to talk to the basestation API. - public GPSWaypointService(HttpClient httpClient) - { - // Assign member variables. - _HttpClient = httpClient; - } + /// + /// Constructor + /// + /// Implicitly passed in, used to talk to the basestation API. + public GPSWaypointService(HttpClient httpClient) + { + // Assign member variables. + _HttpClient = httpClient; + } - /// - /// Refreshes the cached waypoints list from the API database. - /// - /// - public async Task RefreshGPSWaypoints() + /// + /// Refreshes the cached waypoints list from the API database. + /// + /// + public async Task RefreshGPSWaypoints() + { + List? waypoints = await _HttpClient.GetFromJsonAsync>("http://localhost:5000/api/GPSWaypoint"); + if (waypoints is not null) { - List? waypoints = await _HttpClient.GetFromJsonAsync>("http://localhost:5000/api/GPSWaypoint"); - if (waypoints is not null) - { - _gpsWaypoints = waypoints; - } - - // Invoke the callback to refresh page data. - await SyncWaypointsNotifier!.Invoke(); + _gpsWaypoints = waypoints; } - /// - /// Add a callback to get invoked when the waypoints list changes. - /// - /// The method callback to add. - public void SubscribeToWaypointsChanges(SyncWaypointsCallback callback) - { - SyncWaypointsNotifier += callback; - } + // Invoke the callback to refresh page data. + await SyncWaypointsNotifier!.Invoke(); + } - /// - /// Remove a callback from getting invoked when the waypoints list changes. - /// - /// The method callback to remove. - public void UnsubscribeFromWaypointsChanges(SyncWaypointsCallback callback) - { - SyncWaypointsNotifier -= callback; - } + /// + /// Add a callback to get invoked when the waypoints list changes. + /// + /// The method callback to add. + public void SubscribeToWaypointsChanges(SyncWaypointsCallback callback) + { + SyncWaypointsNotifier += callback; + } - /// - /// Add a new waypoint to the database. - /// - /// The waypoint to add. - /// - public async Task AddGPSWaypoint(GPSWaypoint waypoint) - { - // Add the waypoint to the database with the API. - await _HttpClient.PutAsJsonAsync($"http://localhost:5000/api/GPSWaypoint", waypoint); - // Refresh data. - await RefreshGPSWaypoints(); - } + /// + /// Remove a callback from getting invoked when the waypoints list changes. + /// + /// The method callback to remove. + public void UnsubscribeFromWaypointsChanges(SyncWaypointsCallback callback) + { + SyncWaypointsNotifier -= callback; + } - /// - /// Update a waypoint in the database. - /// - /// The waypoint to update. ID must match an existing ID. - /// - public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) - { - // Write the waypoint data to the database with the API. - await _HttpClient.PostAsJsonAsync($"http://localhost:5000/api/GPSWaypoint", waypoint); - // Refresh data. - await RefreshGPSWaypoints(); - } + /// + /// Add a new waypoint to the database. + /// + /// The waypoint to add. + /// + public async Task AddGPSWaypoint(GPSWaypoint waypoint) + { + // Add the waypoint to the database with the API. + await _HttpClient.PutAsJsonAsync($"http://localhost:5000/api/GPSWaypoint", waypoint); + // Refresh data. + await RefreshGPSWaypoints(); + } - /// - /// Given a waypoint ID delete it from the database. - /// - /// The waypoint to delete. - /// - public async Task DeleteGPSWaypoint(GPSWaypoint waypoint) - { - // Delete the waypoint from the database. - await _HttpClient.DeleteAsync($"http://localhost:5000/api/GPSWaypoint/{waypoint.ID}"); - // Refresh data. - await RefreshGPSWaypoints(); - } + /// + /// Update a waypoint in the database. + /// + /// The waypoint to update. ID must match an existing ID. + /// + public async Task UpdateGPSWaypoint(GPSWaypoint waypoint) + { + // Write the waypoint data to the database with the API. + await _HttpClient.PostAsJsonAsync($"http://localhost:5000/api/GPSWaypoint", waypoint); + // Refresh data. + await RefreshGPSWaypoints(); + } - /// - /// Return a reference to the list of GPSWaypoints. - /// - /// - public List GetGPSWaypoints() - { - return _gpsWaypoints; - } + /// + /// Given a waypoint ID delete it from the database. + /// + /// The waypoint to delete. + /// + public async Task DeleteGPSWaypoint(GPSWaypoint waypoint) + { + // Delete the waypoint from the database. + await _HttpClient.DeleteAsync($"http://localhost:5000/api/GPSWaypoint/{waypoint.ID}"); + // Refresh data. + await RefreshGPSWaypoints(); + } - /// - /// Returns the waypoint with the given ID. - /// - /// The ID of the waypoint to retrieve. - /// - public GPSWaypoint? GetGPSWaypoint(int waypointID) - { - return _gpsWaypoints.FirstOrDefault(x => x.ID == waypointID); - } + /// + /// Return a reference to the list of GPSWaypoints. + /// + /// + public List GetGPSWaypoints() + { + return _gpsWaypoints; + } + + /// + /// Returns the waypoint with the given ID. + /// + /// The ID of the waypoint to retrieve. + /// + public GPSWaypoint? GetGPSWaypoint(int waypointID) + { + return _gpsWaypoints.FirstOrDefault(x => x.ID == waypointID); } } \ No newline at end of file diff --git a/Basestation_Software.Web/Core/Services/MapTileService.cs b/Basestation_Software.Web/Core/Services/MapTileService.cs index 99f02f7..45710a0 100644 --- a/Basestation_Software.Web/Core/Services/MapTileService.cs +++ b/Basestation_Software.Web/Core/Services/MapTileService.cs @@ -1,92 +1,91 @@ using Basestation_Software.Models.Geospatial; -namespace Basestation_Software.Web.Core.Services +namespace Basestation_Software.Web.Core.Services; + +public class MapTileService { - public class MapTileService + // Declare member variables. + private readonly HttpClient _HttpClient; + + + /// + /// Constructor + /// + /// Implicitly passed in, used to talk to the basestation API. + public MapTileService(HttpClient httpClient) { - // Declare member variables. - private readonly HttpClient _HttpClient; - + // Assign member variables. + _HttpClient = httpClient; + } - /// - /// Constructor - /// - /// Implicitly passed in, used to talk to the basestation API. - public MapTileService(HttpClient httpClient) - { - // Assign member variables. - _HttpClient = httpClient; - } + /// + /// Add a new tile to the database. + /// + /// The tile to add. + /// + public async Task AddMapTile(MapTile tile) + { + // Add the tile to the database with the API. + await _HttpClient.PutAsJsonAsync($"http://localhost:5000/api/MapTiles", tile); + } - /// - /// Add a new tile to the database. - /// - /// The tile to add. - /// - public async Task AddMapTile(MapTile tile) - { - // Add the tile to the database with the API. - await _HttpClient.PutAsJsonAsync($"http://localhost:5000/api/MapTiles", tile); - } + /// + /// Update a tile in the database. + /// + /// The tile to update. ID must match an existing ID. + /// + public async Task UpdateMapTile(MapTile tile) + { + // Write the tile data to the database with the API. + await _HttpClient.PostAsJsonAsync($"http://localhost:5000/api/MapTiles", tile); + } + + /// + /// Given a tile ID delete it from the database. + /// + /// The tile to delete. + /// + public async Task DeleteMapTile(MapTile tile) + { + // Delete the tile from the database. + await _HttpClient.DeleteAsync($"http://localhost:5000/api/MapTiles/{tile.ID}"); + } - /// - /// Update a tile in the database. - /// - /// The tile to update. ID must match an existing ID. - /// - public async Task UpdateMapTile(MapTile tile) + /// + /// Returns the tile image with the given x, y, z + /// + /// + /// + /// + /// + public async Task GetMapTileImage(int x, int y, int z) + { + try { - // Write the tile data to the database with the API. - await _HttpClient.PostAsJsonAsync($"http://localhost:5000/api/MapTiles", tile); + return await _HttpClient.GetFromJsonAsync($"http://localhost:5000/api/MapTiles/{z}/{y}/{x}.png"); } - - /// - /// Given a tile ID delete it from the database. - /// - /// The tile to delete. - /// - public async Task DeleteMapTile(MapTile tile) + catch (Exception) { - // Delete the tile from the database. - await _HttpClient.DeleteAsync($"http://localhost:5000/api/MapTiles/{tile.ID}"); + return null; } + } - /// - /// Returns the tile image with the given x, y, z - /// - /// - /// - /// - /// - public async Task GetMapTileImage(int x, int y, int z) + /// + /// Returns the tile with the given x, y, z + /// + /// + /// + /// + /// + public async Task GetMapTile(int x, int y, int z) + { + try { - try - { - return await _HttpClient.GetFromJsonAsync($"http://localhost:5000/api/MapTiles/{z}/{y}/{x}.png"); - } - catch (Exception) - { - return null; - } + return await _HttpClient.GetFromJsonAsync($"http://localhost:5000/api/MapTiles/{z}/{y}/{x}"); } - - /// - /// Returns the tile with the given x, y, z - /// - /// - /// - /// - /// - public async Task GetMapTile(int x, int y, int z) + catch (Exception) { - try - { - return await _HttpClient.GetFromJsonAsync($"http://localhost:5000/api/MapTiles/{z}/{y}/{x}"); - } - catch (Exception) - { - return null; - } + return null; } } } \ No newline at end of file diff --git a/Basestation_Software.Web/Core/Services/RoveComm/RoveCommService.cs b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommService.cs new file mode 100644 index 0000000..295c1df --- /dev/null +++ b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommService.cs @@ -0,0 +1,362 @@ +using Basestation_Software.Models.RoveComm; + +namespace Basestation_Software.Web.Core.Services.RoveComm; + +public class RoveCommService : IHostedService +{ + + public RoveCommUDP UDP; + public RoveCommTCP TCP; + + private readonly CancellationTokenSource _cts = new(); + private readonly ILogger _logger; + + public RoveCommService(ILogger logger) + { + _logger = logger; + UDP = new RoveCommUDP(_logger); + TCP = new RoveCommTCP(_logger); + } + + public Task StartAsync(CancellationToken cancelToken) + { + Begin(cancelToken); + SubscribeAll(); + return Task.CompletedTask; + } + + public void Begin(CancellationToken cancelToken) + { + _logger.LogInformation("Starting RoveComm."); + UDP.Begin(cancelToken); + TCP.Begin(cancelToken); + } + + /// + /// Attach the given callback to be triggered when a RoveCommPacket with the given DataID is received. + /// + /// The DataID to listen for. + /// The function to call when the DataID is received. + /// Thrown if the type was invalid. + public void On(int dataId, RoveCommCallback handler) + { + TCP.On(dataId, handler); + UDP.On(dataId, handler); + _logger.LogInformation("Subscribed to {DataID} with type {DataType}.", dataId, RoveCommUtils.DataTypeFromType(typeof(T))); + } + + /// + /// Attach the given callback to be triggered when a RoveCommPacket from the Manifest is received. + /// + /// The name of the board as shown in the Manifest. + /// The name of the Telemetry or Error message as shown in the Manifest. + /// The function to call when the DataID is received. + /// + /// Thrown if the packet descriptor was not found in the Manifest or did not match the given type. + /// + public void On(string boardName, string dataIdString, RoveCommCallback handler) + { + RoveCommUtils.FindDataIDByName(boardName, dataIdString, out var boardDesc, out var packetDesc); + if (boardDesc is null) + { + throw new RoveCommException($"Failed to subscribe to RoveComm: {boardName} Board not found in RoveCommManifest."); + } + else if (packetDesc is null) + { + throw new RoveCommException($"Failed to subscribe to RoveComm: {dataIdString} not found for {boardName} Board."); + } + + RoveCommDataType handlerType = RoveCommUtils.DataTypeFromType(typeof(T)); + if (packetDesc.DataType != handlerType) + { + throw new RoveCommException($"Failed to subscribe to RoveComm: {handlerType} does not match type of {dataIdString} ({packetDesc.DataType})."); + } + + On(packetDesc.DataID, handler); + } + + /// + /// Clear the given callback from all DataID's. + /// + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(RoveCommCallback handler) + { + UDP.Clear(handler); + TCP.Clear(handler); + } + + /// + /// Clear the given callback from the given DataID. + /// + /// The DataID to remove the callback from. + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(int dataId, RoveCommCallback handler) + { + UDP.Clear(dataId, handler); + TCP.Clear(dataId, handler); + _logger.LogInformation("Unsubscribed from {DataID} with type {DataType}.", dataId, RoveCommUtils.DataTypeFromType(typeof(T))); + } + + /// + /// Clear the given callback from the given Manifest entry. + /// + /// The name of the board as shown in the Manifest. + /// The name of the Telemetry or Error message as shown in the Manifest. + /// The callback to remove. + /// /// + /// Thrown if the packet descriptor was not found in the Manifest or did not match the given type. + /// + public void Clear(string boardName, string dataIdString, RoveCommCallback handler) + { + RoveCommUtils.FindDataIDByName(boardName, dataIdString, out var boardDesc, out var packetDesc); + if (boardDesc is null) + { + throw new RoveCommException($"Failed to unsubscribe from RoveComm: {boardName} Board not found in RoveCommManifest."); + } + else if (packetDesc is null) + { + throw new RoveCommException($"Failed to unsubscribe from RoveComm: {dataIdString} not found for {boardName} Board."); + } + + RoveCommDataType handlerType = RoveCommUtils.DataTypeFromType(typeof(T)); + if (packetDesc.DataType != handlerType) + { + throw new RoveCommException($"Failed to unsubscribe from RoveComm: {handlerType} does not match type of {dataIdString} ({packetDesc.DataType})."); + } + + Clear(packetDesc.DataID, handler); + } + + /// + /// Send data over RoveComm. + /// + /// The DataID of the packet. + /// The Data to send. + /// The IP to send to. + /// The port to send to. + /// Send over TCP if true, send over UDP if false. + /// True if the packet was sent successfully. + public bool Send(int dataId, List data, string ip, int port, bool reliable = false) + { + var packet = new RoveCommPacket(dataId, data); + return reliable ? TCP.Send(packet, ip, port) : UDP.Send(packet, ip, port); + } + public bool Send(int dataId, List data, string ip, bool reliable = false) + { + var packet = new RoveCommPacket(dataId, data); + return reliable ? TCP.Send(packet, ip) : UDP.Send(packet, ip); + } + + /// + /// Send data asynchronously over RoveComm. + /// + /// The DataID of the packet. + /// The Data to send. + /// The IP to send to. + /// The port to send to. + /// Send over TCP if true, send over UDP if false. + /// True if the packet was sent successfully. + public async Task SendAsync(int dataId, List data, string ip, int port, bool reliable = false, CancellationToken cancelToken = default) + { + var packet = new RoveCommPacket(dataId, data); + return reliable ? await TCP.SendAsync(packet, ip, port, cancelToken) : await UDP.SendAsync(packet, ip, port, cancelToken); + } + public async Task SendAsync(int dataId, List data, string ip, bool reliable = false, CancellationToken cancelToken = default) + { + var packet = new RoveCommPacket(dataId, data); + return reliable ? await TCP.SendAsync(packet, ip, cancelToken) : await UDP.SendAsync(packet, ip, cancelToken); + } + + /// + /// Send a command from the Manifest over RoveComm. + /// + /// The name of the board as shown in the Manifest. + /// The name of the command as shown in the Manifest. + /// The data to send with the command. + /// Send over TCP if true, send over UDP if false. + /// True if the packet was sent successfully. + /// + /// Thrown if the packet descriptor was not found in the Manifest or did not match the Manifest's schema. + /// + public bool Send(string boardName, string commandName, List data, bool reliable = false) + { + RoveCommUtils.FindDataIDByName(boardName, commandName, out var boardDesc, out var packetDesc); + if (boardDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {boardName} Board not found in RoveCommManifest."); + } + else if (packetDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {commandName} not found for {boardName} Board."); + } + + RoveCommDataType handlerType = RoveCommUtils.DataTypeFromType(typeof(T)); + if (packetDesc.DataType != handlerType) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {handlerType} does not match type of {commandName} ({packetDesc.DataType})."); + } + + if (data.Count != packetDesc.DataCount) + { + throw new RoveCommException($"Failed to send RoveCommPacket: incorrect data size to fill {commandName}."); + } + + return Send(packetDesc.DataID, data, boardDesc.IP, reliable); + } + + /// + /// Send a command from the Manifest over RoveComm asynchronously. + /// + /// The name of the board as shown in the Manifest. + /// The name of the command as shown in the Manifest. + /// The data to send with the command. + /// Send over TCP if true, send over UDP if false. + /// True if the packet was sent successfully. + /// + /// Thrown if the packet descriptor was not found in the Manifest or did not match the Manifest's schema. + /// + public async Task SendAsync(string boardName, string commandName, List data, bool reliable = false, CancellationToken cancelToken = default) + { + RoveCommUtils.FindDataIDByName(boardName, commandName, out var boardDesc, out var packetDesc); + if (boardDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {boardName} Board not found in RoveCommManifest."); + } + else if (packetDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {commandName} not found for {boardName} Board."); + } + + RoveCommDataType handlerType = RoveCommUtils.DataTypeFromType(typeof(T)); + if (packetDesc.DataType != handlerType) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {handlerType} does not match type of {commandName} ({packetDesc.DataType})."); + } + + if (data.Count != packetDesc.DataCount) + { + throw new RoveCommException($"Failed to send RoveCommPacket: incorrect data size to fill {commandName}."); + } + + return await SendAsync(packetDesc.DataID, data, boardDesc.IP, reliable, cancelToken); + } + + /// + /// Wait asynchronously until a packet with the desired DataID arrives. + /// + /// The DataID to listen for. + /// The number of milliseconds before returning null. + /// The RoveCommPacket if received within the timeout, null if none. + public async Task?> Listen(int dataId, int timeout = 30_000) + { + var promise = new TaskCompletionSource>(); + RoveCommCallback callback = async (packet) => + { + promise.SetResult(packet); + await Task.CompletedTask; + }; + + On(dataId, callback); + + var cts = new CancellationTokenSource(); + var cancel = cts.Token; + cts.CancelAfter(timeout); + + try + { + return await promise.Task.WaitAsync(cancel); + } + catch (OperationCanceledException) + { + return null; + } + finally + { + Clear(dataId, callback); + } + } + + /// + /// Wait asynchronously until the desired packet from the Manifest arrives. + /// + /// The name of the board as shown in the Manifest. + /// The name of the Telemetry or Error message as shown in the Manifest. + /// The number of milliseconds before returning null. + /// The RoveCommPacket if received within the timeout, null if none. + public async Task?> Listen(string boardName, string dataIdString, int timeout = 30_000) + { + RoveCommUtils.FindDataIDByName(boardName, dataIdString, out var boardDesc, out var packetDesc); + if (boardDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {boardName} Board not found in RoveCommManifest."); + } + else if (packetDesc is null) + { + throw new RoveCommException($"Failed to send RoveCommPacket: {dataIdString} not found for {boardName} Board."); + } + + return await Listen(packetDesc.DataID, timeout); + } + + public void Subscribe(string boardName) + { + if (RoveCommManifest.Boards.ContainsKey(boardName)) + { + Send(RoveCommManifest.SystemPackets.SUBSCRIBE, [1], RoveCommManifest.Boards[boardName].IP); + } + } + public void SubscribeAll() + { + foreach (var board in RoveCommManifest.Boards.Values) + { + Send(RoveCommManifest.SystemPackets.SUBSCRIBE, [1], board.IP); + } + } + + public void Unubscribe(string boardName) + { + if (RoveCommManifest.Boards.ContainsKey(boardName)) + { + Send(RoveCommManifest.SystemPackets.UNSUBSCRIBE, [1], RoveCommManifest.Boards[boardName].IP); + } + } + public void UnsubscribeAll() + { + foreach (var board in RoveCommManifest.Boards.Values) + { + Send(RoveCommManifest.SystemPackets.UNSUBSCRIBE, [1], board.IP); + } + } + + public void Ping(string boardName) + { + if (RoveCommManifest.Boards.ContainsKey(boardName)) + { + Send(RoveCommManifest.SystemPackets.PING, [1], RoveCommManifest.Boards[boardName].IP); + } + } + public void PingAll() + { + foreach (var board in RoveCommManifest.Boards.Values) + { + Send(RoveCommManifest.SystemPackets.PING, [1], board.IP); + } + } + + public Task StopAsync(CancellationToken cancelToken) + { + UnsubscribeAll(); + Stop(); + return Task.CompletedTask; + } + + public void Stop() + { + _logger.LogInformation("Closing RoveComm."); + _cts.Cancel(); + UDP.Dispose(); + TCP.Dispose(); + } +} diff --git a/Basestation_Software.Web/Core/Services/RoveComm/RoveCommTCP.cs b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommTCP.cs new file mode 100644 index 0000000..523ab50 --- /dev/null +++ b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommTCP.cs @@ -0,0 +1,577 @@ +using System.Net; +using System.Net.Sockets; +using Basestation_Software.Models.RoveComm; + +namespace Basestation_Software.Web.Core.Services.RoveComm; + +public class RoveCommTCP +{ + public static readonly int TCPConnectionTimeout = 10_000; + public static readonly int TCPMaxClients = 10; + + + public bool Running { get; private set; } + public int Port { get; private set; } + + // TCP connections with RoveComm acting as a client. + private TcpClient?[] _outgoing; + // TCP connections with RoveComm acting as a server. + private List _incoming; + // Stops _removeDisconnectedClients() from removing clients while they're is connecting. + private List _connecting; + + // The socket listening for incoming connections. + private TcpListener? _TCPServer; + + private readonly ILogger? _logger; + + private class RoveCommEmitter + { + public event RoveCommCallback? Notifier; + public void Invoke(RoveCommPacket packet) => Notifier?.Invoke(packet); + } + + private readonly Dictionary> _callbacksInt8 = []; + private readonly Dictionary> _callbacksUInt8 = []; + private readonly Dictionary> _callbacksInt16 = []; + private readonly Dictionary> _callbacksUInt16 = []; + private readonly Dictionary> _callbacksInt32 = []; + private readonly Dictionary> _callbacksUInt32 = []; + private readonly Dictionary> _callbacksFloat = []; + private readonly Dictionary> _callbacksDouble = []; + private readonly Dictionary> _callbacksChar = []; + + public RoveCommTCP(int port, ILogger? logger = null) + { + Port = port; + _logger = logger; + _incoming = new List(); + _outgoing = new TcpClient[TCPMaxClients]; + _connecting = new List(); + } + public RoveCommTCP(ILogger? logger = null) : this(RoveCommConsts.TCPPort, logger) { } + + /// + /// Begin accepting TCP connections and reading packets from the network. + /// + public void Begin(CancellationToken cancelToken) + { + if (Running) + { + _logger?.LogWarning("RoveComm TCP already started."); + return; + } + // Begin listening for TCP connection requests. Allow up to 10 pending requests at once. + _logger?.LogInformation("Starting RoveComm TCP on port {Port}.", Port); + _TCPServer = new TcpListener(IPAddress.Any, Port); + try + { + _TCPServer!.Start(10); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to start RoveComm TCP:"); + return; + } + Running = true; + Task.Run(async () => + { + try + { + while (!cancelToken.IsCancellationRequested) + { + // Accept any incoming connections. + if (_TCPServer!.Pending()) + { + TcpClient client = await _TCPServer!.AcceptTcpClientAsync(cancelToken); + _incoming.Add(client); + _logger?.LogInformation("Accepted connection from {Remote}.", client.Client.RemoteEndPoint); + } + // Read packets and trigger callbacks. + await ReceiveAndCallback(cancelToken); + // Don't hog the async queue. + await Task.Delay(RoveCommConsts.UpdateRate); + } + } + catch (Exception e) + { + _logger?.LogError(e, "An exception occurred in RoveComm UDP:"); + } + finally + { + // Close connections. + Stop(); + } + }, cancelToken); + } + + /// + /// Stop accepting TCP connections and reading packets from the network. + /// + public void Stop() + { + if (!Running) + { + _logger?.LogWarning("RoveComm TCP already stopped."); + return; + } + + Running = false; + // Close all incoming connections. + foreach (var connection in _incoming) + { + connection.Close(); + } + _incoming.Clear(); + // Close all outgoing connections. + for (int i = 0; i < TCPMaxClients; i++) + { + _outgoing[i]?.Close(); + _outgoing[i] = null; + } + // Close listening socket. + try + { + _TCPServer!.Stop(); + } + catch (Exception e) + { + _logger?.LogError(e, "Something went wrong closing RoveComm TCP:"); + return; + } + _logger?.LogInformation("Closed RoveComm TCP."); + } + + // Iterate through incoming and outgoing connections. + private IEnumerable _iterateConnections() + { + foreach (var connection in _incoming) + { + yield return connection; + } + foreach (var connection in _outgoing) + { + if (connection is not null) + { + yield return connection; + } + } + } + + // Find existing connection in conneciton list. + private TcpClient? _findExisting(IPEndPoint remote) + { + foreach (var connection in _iterateConnections()) + { + if (connection is null) + { + continue; + } + + var connectionEp = connection.Client.RemoteEndPoint as IPEndPoint; + if (connectionEp is null) + { + return null; + } + else if (connectionEp.Port == remote.Port && connectionEp.Address.Equals(remote.Address)) + { + // _logger?.LogInformation("Found existing conneciton."); + return connection; + } + } + return null; + } + + // Remove all connections that have disconnected. + private void _removeDisconnectedClients() + { + // Remove all closed incoming connections. + _incoming.RemoveAll((connection) => { + if (!_connecting.Contains(connection) && connection.Client.Connected) + { + _logger?.LogInformation("Disconnected from {Remote}.", connection.Client.RemoteEndPoint as IPEndPoint); + connection.Dispose(); + return true; + } + else + { + return false; + } + }); + // Remove all closed outgoing connections. + for (int i = 0; i < TCPMaxClients; i++) + { + var connection = _outgoing[i]; + if (connection is not null && !_connecting.Contains(connection) && !connection.Client.Connected) + { + _logger?.LogInformation("Disconnected from {Remote}.", connection.Client.RemoteEndPoint as IPEndPoint); + connection.Dispose(); + _outgoing[i] = null; + } + }; + } + + // Create a new TcpClient at the first available spot in the list. + private TcpClient? _addNewClient() + { + int freeSpace = Array.IndexOf(_outgoing, null); + if (freeSpace != -1) + { + // Create a new TcpClient with the local endpoint. + // TODO: Add a way to select network interface? + IPAddress localIP = Dns.GetHostEntry(Dns.GetHostName()).AddressList[0]; + int localPort = Port + freeSpace + 1; + IPEndPoint localEndPoint = new IPEndPoint(localIP, localPort); + TcpClient client = new TcpClient(localEndPoint); + _outgoing[freeSpace] = client; + return client; + } + else + { + return null; + } + } + + /// + /// Send a RoveCommPacket to the given IP and port, guaranteeing delivery. + /// + /// The RoveCommPacket to send. + /// The IP to send to. + /// The port to send to. + /// True if the packet was sent successfully. + /// + public bool Send(RoveCommPacket packet, string ip, int port) + { + if (!Running) + { + throw new RoveCommException("Failed to send TCP packet: RoveComm TCP not started."); + } + + _removeDisconnectedClients(); + + var dest = new IPEndPoint(IPAddress.Parse(ip), port); + // Check if there is already a connection with that endpoint. + TcpClient? client = _findExisting(dest); + + // If no existing connection was found, open a new one. + if (client is null) + { + _logger?.LogInformation("Attempting to establish a connection with {Dest}.", dest); + try + { + client = _addNewClient(); + if (client is null) + { + _logger?.LogError("Failed to connect to remote host: Too many TCP connections open."); + return false; + } + + _connecting.Add(client); + if (!client.ConnectAsync(dest).Wait(TimeSpan.FromMilliseconds(TCPConnectionTimeout))) + { + _logger?.LogError("Failed to connect to remote host: The operation has timed out."); + return false; + } + } + catch (Exception e) + { + _logger?.LogError("Failed to connect to remote host: {Error}", e.Message); + return false; + } + finally + { + if (client is not null) + { + _connecting.Remove(client); + } + } + + _logger?.LogInformation("Established connection with {Remote}.", client.Client.RemoteEndPoint as IPEndPoint); + } + // Write the packet to the client's NetworkStream. + try + { + client.GetStream().Write(RoveCommUtils.PackPacket(packet)); + } + catch (Exception e) + { + _logger?.LogError("Failed to send TCP packet: {Error}", e.Message); + return false; + } + _logger?.LogInformation("TCP: Sent RoveCommPacket with DataID {DataID} and type {DataType}[{DataCount}] to {Dest}.", packet.DataID, packet.DataType, packet.DataCount, dest); + return true; + } + public bool Send(RoveCommPacket packet, string ip) => Send(packet, ip, Port); + + /// + /// Send a RoveCommPacket to the given IP and port asynchronously. + /// + /// The RoveCommPacket to send. + /// The IP to send to. + /// The port to send to. + /// True if the packet was sent successfully. + /// + public async Task SendAsync(RoveCommPacket packet, string ip, int port, CancellationToken cancelToken = default) + { + if (!Running) + { + throw new RoveCommException("Failed to send TCP packet: RoveComm TCP not started."); + } + + _removeDisconnectedClients(); + + var dest = new IPEndPoint(IPAddress.Parse(ip), port); + // Check if there is already a connection with that endpoint. + TcpClient? client = _findExisting(dest); + // If no existing connection was found, open a new one. + if (client is null) + { + _logger?.LogInformation("Attempting to establish a connection with {Dest}.", dest); + + try + { + client = _addNewClient(); + if (client is null) + { + _logger?.LogError("Failed to connect to remote host: Too many TCP connections open."); + return false; + } + + _connecting.Add(client); + await client.ConnectAsync(dest).WaitAsync(TimeSpan.FromMilliseconds(TCPConnectionTimeout), cancelToken); + } + catch (Exception e) + { + _logger?.LogError("Failed to connect to remote host: {Error}", e.Message); + return false; + } + finally + { + if (client is not null) + { + _connecting.Remove(client); + } + } + + _logger?.LogInformation("Established connection with {Remote}.", client.Client.RemoteEndPoint as IPEndPoint); + } + // Write the packet to the client's NetworkStream. + try + { + await client.GetStream().WriteAsync(RoveCommUtils.PackPacket(packet), cancelToken); + } + catch (Exception e) + { + _logger?.LogError("Failed to send TCP packet: {Error}", e.Message); + return false; + } + _logger?.LogInformation("TCP: Sent RoveCommPacket with DataID {DataID} and Data {DataType}[{DataCount}] to {Dest}.", packet.DataID, packet.DataType, packet.DataCount, dest); + return true; + } + public async Task SendAsync(RoveCommPacket packet, string ip, CancellationToken cancelToken = default) => + await SendAsync(packet, ip, Port, cancelToken); + + /// + /// Internal method for reading packets off the network and triggering attached callbacks. + /// + public async Task ReceiveAndCallback(CancellationToken cancelToken = default) + { + if (!Running) + { + throw new RoveCommException("Failed to read TCP packet: RoveComm TCP not started."); + } + + _removeDisconnectedClients(); + + foreach (var connection in _iterateConnections()) + { + try + { + // Check if the client has enough bytes available to read a packet. + // TCP is a stream protocol, so if not enough bytes are available now, more will come in later. + if (connection.Client.Available < RoveCommConsts.HeaderSize) + { + continue; + } + // Get a reference to the stream managed by the TcpClient. + NetworkStream stream = connection.GetStream(); + // Quit reading if no new data is received after 30 seconds. + stream.ReadTimeout = 30_000; + // Create byte buffer with max packet size. + byte[] readBuf = new byte[RoveCommConsts.HeaderSize + RoveCommConsts.MaxDataSize]; + // Read header. + var headerBuf = readBuf.AsMemory(0, RoveCommConsts.HeaderSize); + int bytesRead = await stream.ReadAsync(headerBuf, cancelToken); + if (bytesRead == 0) + { + _logger?.LogWarning("Failed to receive TCP data."); + return; + } + + RoveCommHeader header = RoveCommUtils.ParseHeader(readBuf); + RoveCommDataType dataType = RoveCommUtils.ParseDataType(header.DataType); + int dataTypeSize = RoveCommUtils.DataTypeSize(dataType); + int dataSize = header.DataCount * dataTypeSize; + var dataBuf = readBuf.AsMemory(RoveCommConsts.HeaderSize, dataSize); + await stream.ReadAsync(dataBuf, cancelToken); + // Parse packet and trigger callbacks. + int packetSize = RoveCommConsts.HeaderSize + dataSize; + var packetBuf = readBuf.AsMemory(0, packetSize); + switch (dataType) + { + case RoveCommDataType.INT8_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.UINT8_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.INT16_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.UINT16_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.INT32_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.UINT32_T: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.FLOAT: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.DOUBLE: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + case RoveCommDataType.CHAR: ProcessPacket(RoveCommUtils.ParsePacket(packetBuf.Span)); break; + } + _logger?.LogInformation("TCP: Received RoveCommPacket with DataID {DataID} and Data {DataType}[{DataCount}] from {Remote}.", header.DataID, dataType, header.DataCount, connection.Client.RemoteEndPoint as IPEndPoint); + } + // RoveComm couldn't parse something: + catch (RoveCommException e) + { + _logger?.LogError("Failed to read TCP packet: {Error}", e.Message); + } + // Network problems: + catch (Exception e) + { + _logger?.LogError("Failed to receive TCP data: {Error}", e.Message); + } + } + } + + + /// + /// Trigger attached callbacks for the given RoveCommPacket. + /// + /// The RoveCommPacket to pass to the callbacks. + /// Thrown if the type was invalid. + public void ProcessPacket(RoveCommPacket packet) + { + switch (packet) + { + case RoveCommPacket p: _processPacket(_callbacksInt8, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt8, p); break; + case RoveCommPacket p: _processPacket(_callbacksInt16, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt16, p); break; + case RoveCommPacket p: _processPacket(_callbacksInt32, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt32, p); break; + case RoveCommPacket p: _processPacket(_callbacksFloat, p); break; + case RoveCommPacket p: _processPacket(_callbacksDouble, p); break; + case RoveCommPacket p: _processPacket(_callbacksChar, p); break; + default: throw new RoveCommException("Failed to process RoveCommPacket: invalid data type."); + } + } + private void _processPacket(Dictionary> callbacks, RoveCommPacket packet) + { + if (callbacks.ContainsKey(packet.DataID)) + { + callbacks[packet.DataID].Invoke(packet); + } + // Data ID 0 means subscribe to all packets. + if (callbacks.ContainsKey(0)) + { + callbacks[0].Invoke(packet); + } + } + + /// + /// Attach the given callback to be triggered when a RoveCommPacket with the given DataID is received. + /// + /// The DataID to listen for. + /// The function to call when the DataID is received. + /// Thrown if the type was invalid. + public void On(int dataId, RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _addCallback(_callbacksInt8, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt8, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksInt16, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt16, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksInt32, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt32, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksFloat, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksDouble, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksChar, dataId, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _addCallback(Dictionary> callbacks, int dataId, RoveCommCallback handler) + { + if (!callbacks.ContainsKey(dataId)) + { + callbacks.Add(dataId, new RoveCommEmitter()); + } + + callbacks[dataId].Notifier += handler; + } + + /// + /// Clear the given callback from all DataID's. + /// + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _clearCallback(_callbacksInt8, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt8, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt16, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt16, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt32, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt32, h); break; + case RoveCommCallback h: _clearCallback(_callbacksFloat, h); break; + case RoveCommCallback h: _clearCallback(_callbacksDouble, h); break; + case RoveCommCallback h: _clearCallback(_callbacksChar, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _clearCallback(Dictionary> callbacks, RoveCommCallback handler) + { + foreach (var emitter in callbacks.Values) + { + emitter.Notifier -= handler; + } + } + + /// + /// Clear the given callback from the given DataID. + /// + /// The DataID to remove the callback from. + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(int dataId, RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _clearCallback(_callbacksInt8, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt8, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt16, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt16, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt32, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt32, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksFloat, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksDouble, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksChar, dataId, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _clearCallback(Dictionary> callbacks, int dataId, RoveCommCallback handler) + { + if (!callbacks.ContainsKey(dataId)) + { + callbacks[dataId].Notifier -= handler; + } + } + + public void Dispose() + { + Stop(); + } +} diff --git a/Basestation_Software.Web/Core/Services/RoveComm/RoveCommUDP.cs b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommUDP.cs new file mode 100644 index 0000000..2c8604a --- /dev/null +++ b/Basestation_Software.Web/Core/Services/RoveComm/RoveCommUDP.cs @@ -0,0 +1,364 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using Basestation_Software.Models.RoveComm; + +namespace Basestation_Software.Web.Core.Services.RoveComm; + +public class RoveCommUDP : IDisposable +{ + public bool Running { get; private set; } + public int Port { get; private set; } + private UdpClient? _UDPServer; + + private readonly ILogger? _logger; + + private class RoveCommEmitter + { + public event RoveCommCallback? Notifier; + public void Invoke(RoveCommPacket packet) => Notifier?.Invoke(packet); + } + + private readonly Dictionary> _callbacksInt8 = []; + private readonly Dictionary> _callbacksUInt8 = []; + private readonly Dictionary> _callbacksInt16 = []; + private readonly Dictionary> _callbacksUInt16 = []; + private readonly Dictionary> _callbacksInt32 = []; + private readonly Dictionary> _callbacksUInt32 = []; + private readonly Dictionary> _callbacksFloat = []; + private readonly Dictionary> _callbacksDouble = []; + private readonly Dictionary> _callbacksChar = []; + + public RoveCommUDP(int port, ILogger? logger = null) + { + Port = port; + _logger = logger; + } + public RoveCommUDP(ILogger? logger = null) : this(RoveCommConsts.UDPPort, logger) { } + + /// + /// Begin accepting UDP connections and reading packets from the network. + /// + public void Begin(CancellationToken cancelToken) + { + if (Running) + { + _logger?.LogWarning("RoveComm UDP already started."); + return; + } + + // Open new UDP socket. + _logger?.LogInformation("Starting RoveComm UDP on port {Port}.", Port); + try + { + _UDPServer = new UdpClient(Port, AddressFamily.InterNetwork); + } + catch (Exception e) + { + _logger?.LogError(e, "Failed to start RoveComm UDP:"); + return; + } + Running = true; + Task.Run(async () => + { + try + { + while (!cancelToken.IsCancellationRequested) + { + // Read packets and trigger callbacks. + await ReceiveAndCallback(cancelToken); + // Don't hog the async queue. + await Task.Delay(RoveCommConsts.UpdateRate); + } + } + catch (Exception e) + { + _logger?.LogError(e, "An exception occurred in RoveComm UDP:"); + } + finally + { + // Close UDP socket. + Stop(); + } + }, cancelToken); + } + + public void Stop() + { + if (!Running) + { + _logger?.LogWarning("RoveComm UDP already stopped."); + return; + } + + Running = false; + // Close UDP socket; + try + { + _UDPServer!.Close(); + } + catch (Exception e) + { + _logger?.LogError(e, "Something went wrong closing RoveComm UDP:"); + return; + } + _UDPServer = null; + _logger?.LogInformation("Closed RoveComm UDP."); + } + + /// + /// Send a RoveCommPacket to the given IP and port. + /// + /// The RoveCommPacket to send. + /// The IP to send to. + /// The port to send to. + /// True if the packet was sent successfully. + /// + public bool Send(RoveCommPacket packet, string ip, int port) + { + if (!Running) + { + throw new RoveCommException("Failed to send UDP packet: RoveComm UDP not started."); + } + + var dest = new IPEndPoint(IPAddress.Parse(ip), port); + try + { + int bytesSent = _UDPServer!.Send(RoveCommUtils.PackPacket(packet), dest); + int expected = RoveCommConsts.HeaderSize + packet.DataCount * RoveCommUtils.DataTypeSize(packet.DataType); + if (bytesSent != expected) + { + _logger?.LogError("Failed to send UDP packet: {Sent} of {Expected} bytes sent.", bytesSent, expected); + return false; + } + } + catch (Exception e) + { + _logger?.LogError("Failed to send UDP packet: {Error}", e.Message); + return false; + } + + _logger?.LogInformation("UDP: Sent RoveCommPacket with DataID {DataID} and Data {DataType}[{DataCount}] to {Dest}.", packet.DataID, packet.DataType, packet.DataCount, dest); + return true; + } + public bool Send(RoveCommPacket packet, string ip) => Send(packet, ip, Port); + + /// + /// Send a RoveCommPacket to the given IP and port asynchronously. + /// + /// The RoveCommPacket to send. + /// The IP to send to. + /// The port to send to. + /// True if the packet was sent successfully. + /// + public async Task SendAsync(RoveCommPacket packet, string ip, int port, CancellationToken cancelToken = default) + { + if (!Running) + { + throw new RoveCommException("Failed to send UDP packet: RoveComm UDP not started."); + } + + var dest = new IPEndPoint(IPAddress.Parse(ip), port); + try + { + int bytesSent = await _UDPServer!.SendAsync(RoveCommUtils.PackPacket(packet), dest, cancelToken); + int expected = RoveCommConsts.HeaderSize + packet.DataCount * RoveCommUtils.DataTypeSize(packet.DataType); + if (bytesSent != expected) + { + _logger?.LogError("Failed to send UDP packet: {Sent} of {Expected} bytes sent.", bytesSent, expected); + return false; + } + } + catch (Exception e) + { + _logger?.LogError("Failed to send UDP packet: {Error}", e.Message); + return false; + } + + _logger?.LogInformation("UDP: Sent RoveCommPacket with DataID {DataID} and Data {DataType}[{DataCount}] to {Dest}.", packet.DataID, packet.DataType, packet.DataCount, dest); + return true; + } + public async Task SendAsync(RoveCommPacket packet, string ip, CancellationToken cancelToken = default) => + await SendAsync(packet, ip, Port, cancelToken); + + /// + /// Internal method for reading packets off the network and triggering attached callbacks. + /// + public async Task ReceiveAndCallback(CancellationToken cancelToken) + { + if (!Running) + { + throw new RoveCommException("Failed to read UDP packet: RoveComm UDP not started."); + } + + try + { + // We still want to process datagrams which are too small to parse; we just discard them. + if (_UDPServer!.Available == 0) + { + return; + } + + var result = await _UDPServer!.ReceiveAsync(cancelToken); + IPEndPoint fromIP = result.RemoteEndPoint; + byte[] data = result.Buffer; + // If there aren't enough bytes to parse the header, the error will be caught and logged. + RoveCommHeader header = RoveCommUtils.ParseHeader(data); + RoveCommDataType dataType = RoveCommUtils.ParseDataType(header.DataType); + switch (dataType) + { + case RoveCommDataType.INT8_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.UINT8_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.INT16_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.UINT16_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.INT32_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.UINT32_T: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.FLOAT: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.DOUBLE: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + case RoveCommDataType.CHAR: ProcessPacket(RoveCommUtils.ParsePacket(data)); break; + } + _logger?.LogInformation("UDP: Received RoveCommPacket with DataID {DataID} and Data {DataType}[{DataCount}] from {Remote}.", header.DataID, dataType, header.DataCount, fromIP); + } + // RoveComm couldn't parse something: + catch (RoveCommException e) + { + _logger?.LogError("Failed to read UDP packet: {Error}", e.Message); + } + // Network problems: + catch (Exception e) + { + _logger?.LogError("Failed to receive UDP data: {Error}", e.Message); + } + } + + /// + /// Trigger attached callbacks for the given RoveCommPacket. + /// + /// The RoveCommPacket to pass to the callbacks. + /// Thrown if the type was invalid. + public void ProcessPacket(RoveCommPacket packet) + { + switch (packet) + { + case RoveCommPacket p: _processPacket(_callbacksInt8, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt8, p); break; + case RoveCommPacket p: _processPacket(_callbacksInt16, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt16, p); break; + case RoveCommPacket p: _processPacket(_callbacksInt32, p); break; + case RoveCommPacket p: _processPacket(_callbacksUInt32, p); break; + case RoveCommPacket p: _processPacket(_callbacksFloat, p); break; + case RoveCommPacket p: _processPacket(_callbacksDouble, p); break; + case RoveCommPacket p: _processPacket(_callbacksChar, p); break; + default: throw new RoveCommException("Failed to process RoveCommPacket: invalid data type."); + } + } + private void _processPacket(Dictionary> callbacks, RoveCommPacket packet) + { + if (callbacks.ContainsKey(packet.DataID)) + { + callbacks[packet.DataID].Invoke(packet); + } + // Data ID 0 means subscribe to all packets. + if (callbacks.ContainsKey(0)) + { + callbacks[0].Invoke(packet); + } + } + + /// + /// Attach the given callback to be triggered when a RoveCommPacket with the given DataID is received. + /// + /// The DataID to listen for. + /// The function to call when the DataID is received. + /// Thrown if the type was invalid. + public void On(int dataId, RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _addCallback(_callbacksInt8, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt8, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksInt16, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt16, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksInt32, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksUInt32, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksFloat, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksDouble, dataId, h); break; + case RoveCommCallback h: _addCallback(_callbacksChar, dataId, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _addCallback(Dictionary> callbacks, int dataId, RoveCommCallback handler) + { + if (!callbacks.ContainsKey(dataId)) + { + callbacks.Add(dataId, new RoveCommEmitter()); + } + + callbacks[dataId].Notifier += handler; + } + + + /// + /// Clear the given callback from all DataID's. + /// + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _clearCallback(_callbacksInt8, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt8, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt16, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt16, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt32, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt32, h); break; + case RoveCommCallback h: _clearCallback(_callbacksFloat, h); break; + case RoveCommCallback h: _clearCallback(_callbacksDouble, h); break; + case RoveCommCallback h: _clearCallback(_callbacksChar, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _clearCallback(Dictionary> callbacks, RoveCommCallback handler) + { + foreach (var emitter in callbacks.Values) + { + emitter.Notifier -= handler; + } + } + + /// + /// Clear the given callback from the given DataID. + /// + /// The DataID to remove the callback from. + /// The callback to remove. + /// Thrown if the type was invalid. + public void Clear(int dataId, RoveCommCallback handler) + { + switch (handler) + { + case RoveCommCallback h: _clearCallback(_callbacksInt8, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt8, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt16, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt16, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksInt32, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksUInt32, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksFloat, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksDouble, dataId, h); break; + case RoveCommCallback h: _clearCallback(_callbacksChar, dataId, h); break; + default: throw new RoveCommException("Failed to add callback: invalid data type."); + } + } + private void _clearCallback(Dictionary> callbacks, int dataId, RoveCommCallback handler) + { + if (!callbacks.ContainsKey(dataId)) + { + callbacks[dataId].Notifier -= handler; + } + } + + public void Dispose() + { + Stop(); + } +} \ No newline at end of file diff --git a/Basestation_Software.Web/Core/Services/TaskTimerService.cs b/Basestation_Software.Web/Core/Services/TaskTimerService.cs index 9cb2add..ba91cc0 100644 --- a/Basestation_Software.Web/Core/Services/TaskTimerService.cs +++ b/Basestation_Software.Web/Core/Services/TaskTimerService.cs @@ -1,147 +1,142 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Basestation_Software.Models.Timers; -namespace Basestation_Software.Web.Core.Services +namespace Basestation_Software.Web.Core.Services; + +public class TaskTimerService { - public class TaskTimerService - { - // Declare member variables. - private List _taskTimers = new List(); + // Declare member variables. + private List _taskTimers = []; - // Create a dictionary of event handlers for updating the UIs. - public delegate Task TimerTickCallback(TimeSpan elapsedTime); - public Dictionary TimerTickNotifiers = new Dictionary(); + // Create a dictionary of event handlers for updating the UIs. + public delegate Task TimerTickCallback(TimeSpan elapsedTime); + public Dictionary TimerTickNotifiers = []; - /// - /// Constructor. - /// - public TaskTimerService() + /// + /// Constructor. + /// + public TaskTimerService() + { + // Loop through the task timer types enum. + foreach (TaskType TaskType in Enum.GetValues(typeof(TaskType))) { - // Loop through the task timer types enum. - foreach (TaskType TaskType in Enum.GetValues(typeof(TaskType))) + // Check if a task timer for the current type exists. + TaskTimer? taskTimer = GetTaskTimer(TaskType); + // Check if the timer is null. + if (taskTimer == null) { - // Check if a task timer for the current type exists. - TaskTimer? taskTimer = GetTaskTimer(TaskType); - // Check if the timer is null. - if (taskTimer == null) + // Create a new timer. + TaskTimer newTimer = new(OnTimerTick, TaskType); + // Configure the timer. + switch (TaskType) { - // Create a new timer. - TaskTimer newTimer = new TaskTimer(OnTimerTick, TaskType); - // Configure the timer. - switch (TaskType) - { - case TaskType.Autonomy: - // Total time for the task. - newTimer.EndPoint = TimeSpan.FromSeconds(80); - // Add checkpoints. - newTimer.CheckPoints = new Dictionary - { - { "Setup", TimeSpan.FromSeconds(10) }, - { "Autonomy Task", TimeSpan.FromSeconds(60) }, - { "PackUp", TimeSpan.FromSeconds(10) } - }; - break; - case TaskType.Science: - // Total time for the task. - newTimer.EndPoint = TimeSpan.FromMinutes(50); - // Add checkpoints. - newTimer.CheckPoints = new Dictionary - { - { "Setup", TimeSpan.FromMinutes(10) }, - { "Science Task", TimeSpan.FromMinutes(30) }, - { "PackUp", TimeSpan.FromMinutes(10) }, - }; - break; - case TaskType.ExtremeDelivery: - // Total time for the task. - newTimer.EndPoint = TimeSpan.FromMinutes(80); - // Add checkpoints. - newTimer.CheckPoints = new Dictionary - { - { "Setup", TimeSpan.FromMinutes(10) }, - { "Extreme Retrieval/Delivery Task", TimeSpan.FromMinutes(60) }, - { "PackUp", TimeSpan.FromMinutes(10) }, - }; - break; - case TaskType.EquipmentServicing: - // Total time for the task. - newTimer.EndPoint = TimeSpan.FromMinutes(50); - // Add checkpoints. - newTimer.CheckPoints = new Dictionary - { - { "Setup", TimeSpan.FromMinutes(10) }, - { "Equipment Servicing Task", TimeSpan.FromMinutes(30) }, - { "PackUp", TimeSpan.FromMinutes(10) }, - }; - break; - default: - newTimer.EndPoint = TimeSpan.FromMinutes(1); - break; - } - // Add the timer to the service. - AddTaskTimer(newTimer); + case TaskType.Autonomy: + // Total time for the task. + newTimer.EndPoint = TimeSpan.FromSeconds(80); + // Add checkpoints. + newTimer.CheckPoints = new Dictionary + { + { "Setup", TimeSpan.FromSeconds(10) }, + { "Autonomy Task", TimeSpan.FromSeconds(60) }, + { "PackUp", TimeSpan.FromSeconds(10) } + }; + break; + case TaskType.Science: + // Total time for the task. + newTimer.EndPoint = TimeSpan.FromMinutes(50); + // Add checkpoints. + newTimer.CheckPoints = new Dictionary + { + { "Setup", TimeSpan.FromMinutes(10) }, + { "Science Task", TimeSpan.FromMinutes(30) }, + { "PackUp", TimeSpan.FromMinutes(10) }, + }; + break; + case TaskType.ExtremeDelivery: + // Total time for the task. + newTimer.EndPoint = TimeSpan.FromMinutes(80); + // Add checkpoints. + newTimer.CheckPoints = new Dictionary + { + { "Setup", TimeSpan.FromMinutes(10) }, + { "Extreme Retrieval/Delivery Task", TimeSpan.FromMinutes(60) }, + { "PackUp", TimeSpan.FromMinutes(10) }, + }; + break; + case TaskType.EquipmentServicing: + // Total time for the task. + newTimer.EndPoint = TimeSpan.FromMinutes(50); + // Add checkpoints. + newTimer.CheckPoints = new Dictionary + { + { "Setup", TimeSpan.FromMinutes(10) }, + { "Equipment Servicing Task", TimeSpan.FromMinutes(30) }, + { "PackUp", TimeSpan.FromMinutes(10) }, + }; + break; + default: + newTimer.EndPoint = TimeSpan.FromMinutes(1); + break; } - - // Add the timer tick callback to the dictionary. - TimerTickNotifiers.Add(TaskType, null); + // Add the timer to the service. + AddTaskTimer(newTimer); } - } - /// - /// Runs when a timer ticks. - /// - /// The task type. - /// The current time elapsed in the task. - /// - private async Task OnTimerTick(TaskType timerName, TimeSpan timeElapsed) - { - // Invoke the timer tick callback. - if (TimerTickNotifiers.ContainsKey(timerName) && TimerTickNotifiers[timerName] is not null) - { - await TimerTickNotifiers[timerName]!.Invoke(timeElapsed); - } + // Add the timer tick callback to the dictionary. + TimerTickNotifiers.Add(TaskType, null); } + } - /// - /// Add a task timer to the list of task timers. - /// - /// The task timer object. - public void AddTaskTimer(TaskTimer taskTimer) + /// + /// Runs when a timer ticks. + /// + /// The task type. + /// The current time elapsed in the task. + /// + private async Task OnTimerTick(TaskType timerName, TimeSpan timeElapsed) + { + // Invoke the timer tick callback. + if (TimerTickNotifiers.ContainsKey(timerName) && TimerTickNotifiers[timerName] is not null) { - _taskTimers.Add(taskTimer); + await TimerTickNotifiers[timerName]!.Invoke(timeElapsed); } + } - /// - /// Remove a task timer from the list of task timers. - /// - /// The tasktimer object type to remove. - public void RemoveTaskTimer(TaskType taskTimerType) + /// + /// Add a task timer to the list of task timers. + /// + /// The task timer object. + public void AddTaskTimer(TaskTimer taskTimer) + { + _taskTimers.Add(taskTimer); + } + + /// + /// Remove a task timer from the list of task timers. + /// + /// The tasktimer object type to remove. + public void RemoveTaskTimer(TaskType taskTimerType) + { + TaskTimer? taskTimer = GetTaskTimer(taskTimerType); + if (taskTimer != null) { - TaskTimer? taskTimer = GetTaskTimer(taskTimerType); - if (taskTimer != null) - { - _taskTimers.RemoveAll(t => t == taskTimer); - } + _taskTimers.RemoveAll(t => t == taskTimer); } + } - /// - /// Get a task timer from the list of task timers. - /// - /// The name of the task timer to retrieve. - /// - public TaskTimer? GetTaskTimer(TaskType timerType) + /// + /// Get a task timer from the list of task timers. + /// + /// The name of the task timer to retrieve. + /// + public TaskTimer? GetTaskTimer(TaskType timerType) + { + foreach (TaskTimer taskTimer in _taskTimers) { - foreach (TaskTimer taskTimer in _taskTimers) + if (taskTimer.TaskType == timerType) { - if (taskTimer.TaskType == timerType) - { - return taskTimer; - } + return taskTimer; } - return null; } + return null; } } \ No newline at end of file diff --git a/Basestation_Software.Web/Core/_Imports.razor b/Basestation_Software.Web/Core/_Imports.razor index 7d90958..48a3dc5 100644 --- a/Basestation_Software.Web/Core/_Imports.razor +++ b/Basestation_Software.Web/Core/_Imports.razor @@ -10,9 +10,11 @@ @using Basestation_Software.Web @using Basestation_Software.Web.Core @using Basestation_Software.Web.Core.Services +@using Basestation_Software.Web.Core.Services.RoveComm @using Basestation_Software.Web.Core.Components @using Basestation_Software.Models.Geospatial @using Basestation_Software.Models.Timers +@using Basestation_Software.Models.RoveComm @using Radzen @using Radzen.Blazor @using Blazored.Toast @@ -22,4 +24,5 @@ @using LeafletForBlazor @inject CookieService _CookieService -@inject IToastService _ToastService \ No newline at end of file +@inject IToastService _ToastService +@* @inject RoveCommService RoveComm *@ \ No newline at end of file diff --git a/Basestation_Software.Web/Program.cs b/Basestation_Software.Web/Program.cs index 8bc2503..64254ba 100644 --- a/Basestation_Software.Web/Program.cs +++ b/Basestation_Software.Web/Program.cs @@ -1,8 +1,10 @@ using Basestation_Software.Web.Core; using Basestation_Software.Web.Core.Services; +using Basestation_Software.Web.Core.Services.RoveComm; using Blazored.Toast; using Radzen; +#pragma warning disable IDE0211 // Convert to 'Program.Main' style program var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -10,8 +12,8 @@ .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); builder.Services.AddServerSideBlazor() - .AddCircuitOptions(option => - { + .AddCircuitOptions(option => + { option.DetailedErrors = true; option.DisconnectedCircuitRetentionPeriod = TimeSpan.FromSeconds(10); }) @@ -24,6 +26,8 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService((sp) => sp.GetRequiredService()); builder.Services.AddBlazoredToast(); diff --git a/Basestation_Software.Web/wwwroot/bootstrap-ext.css b/Basestation_Software.Web/wwwroot/bootstrap-ext.css index 1b5a55a..2050338 100644 --- a/Basestation_Software.Web/wwwroot/bootstrap-ext.css +++ b/Basestation_Software.Web/wwwroot/bootstrap-ext.css @@ -3,16 +3,29 @@ color: white; } +.card-header h3 { + font-size: larger; + margin-block: auto; + height: 100%; + vertical-align: middle; +} + +.card-header .vr { + width: 1px; + margin-inline: 10px; + opacity: 100%; +} + .card .read-more:hover { font-size: 14px; } .card:hover { - box-shadow: 8px 8px 8px rgba(225, 3, 3, 0.692); - transform:scale(1.004); + box-shadow: 5px 5px 0px rgba(209, 3, 3, 0.692); +/* transform:scale(1.004);*/ } .card { border:1px solid black; - transition:.3s; + transition:.25s; } \ No newline at end of file diff --git a/Basestation_Software.Web/wwwroot/leaflet.css b/Basestation_Software.Web/wwwroot/leaflet.css new file mode 100644 index 0000000..1981009 --- /dev/null +++ b/Basestation_Software.Web/wwwroot/leaflet.css @@ -0,0 +1,656 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } diff --git a/Basestation_Software.Web/wwwroot/leaflet.js b/Basestation_Software.Web/wwwroot/leaflet.js new file mode 100644 index 0000000..047bfe7 --- /dev/null +++ b/Basestation_Software.Web/wwwroot/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.3, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Ft.firstChild&&Ft.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Ft,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Wt=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Wt,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Wt,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=W(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!Fe(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var Ve,B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section;S(t,"click",O),this.expand(),setTimeout(function(){k(t,"click",O)})}})),Ge=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ke=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ge,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ye).addTo(this)}),B.Layers=qe,B.Zoom=Ge,B.Scale=Ke,B.Attribution=Ye,Ue.layers=function(t,e,i){return new qe(t,e,i)},Ue.zoom=function(t){return new Ge(t)},Ue.scale=function(t){return new Ke(t)},Ue.attribution=function(t){return new Ye(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Xe=b.touch?"touchstart mousedown":"mousedown",Je=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Xe,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Je._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Xe,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Je._dragging===this&&this.finishDrag():Je._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Je._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ni(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||vi.prototype._containsPoint.call(this,t,!0)}});var xi=ui.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Bi=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Ai,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Ai,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ui||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof mi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Oi.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Oi.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Oi.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Oi.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Ni=Ri.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Hi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Ui("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Ui("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Ui("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Ui("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},Vi=b.vml?Ui:ct,qi=Hi.extend({_initContainer:function(){this._container=Vi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=Vi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Hi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=Vi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Gi(t){return b.svg||b.vml?new qi(t):null}b.vml&&qi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Wi(t)||Gi(t)}});var Ki=yi.extend({initialize:function(t,e){yi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});qi.create=Vi,qi.pointsToPath=dt,xi.geometryToLayer=wi,xi.coordsToLatLng=Pi,xi.coordsToLatLngs=Li,xi.latLngToCoords=Ti,xi.latLngsToCoords=Mi,xi.getFeature=zi,xi.asFeature=Ci,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Je(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1'; + } else if (iconCls) { + html = ''; + } + + el.innerHTML = html + options.text; + el.href = '#'; + + L.DomEvent + .on(el, 'mouseover', this._onItemMouseOver, this) + .on(el, 'mouseout', this._onItemMouseOut, this) + .on(el, 'mousedown', L.DomEvent.stopPropagation) + .on(el, 'click', callback); + + if (L.Browser.touch) { + L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation); + } + + // Devices without a mouse fire "mouseover" on tap, but never “mouseout" + if (!L.Browser.pointer) { + L.DomEvent.on(el, 'click', this._onItemMouseOut, this); + } + + return { + id: L.Util.stamp(el), + el: el, + callback: callback + }; + }, + + _removeItem: function (id) { + var item, + el, + i, l, callback; + + for (i = 0, l = this._items.length; i < l; i++) { + item = this._items[i]; + + if (item.id === id) { + el = item.el; + callback = item.callback; + + if (callback) { + L.DomEvent + .off(el, 'mouseover', this._onItemMouseOver, this) + .off(el, 'mouseover', this._onItemMouseOut, this) + .off(el, 'mousedown', L.DomEvent.stopPropagation) + .off(el, 'click', callback); + + if (L.Browser.touch) { + L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation); + } + + if (!L.Browser.pointer) { + L.DomEvent.on(el, 'click', this._onItemMouseOut, this); + } + } + + this._container.removeChild(el); + this._items.splice(i, 1); + + return item; + } + } + return null; + }, + + _createSeparator: function (container, index) { + var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index); + + return { + id: L.Util.stamp(el), + el: el + }; + }, + + _createEventHandler: function (el, func, context, hideOnSelect) { + var me = this, + map = this._map, + disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled', + hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true; + + return function (e) { + if (L.DomUtil.hasClass(el, disabledCls)) { + return; + } + + if (hideOnSelect) { + me._hide(); + } + + if (func) { + func.call(context || map, me._showLocation); + } + + me._map.fire('contextmenu.select', { + contextmenu: me, + el: el + }); + }; + }, + + _insertElementAt: function (tagName, className, container, index) { + var refEl, + el = document.createElement(tagName); + + el.className = className; + + if (index !== undefined) { + refEl = container.children[index]; + } + + if (refEl) { + container.insertBefore(el, refEl); + } else { + container.appendChild(el); + } + + return el; + }, + + _show: function (e) { + this._showAtPoint(e.containerPoint, e); + }, + + _showAtPoint: function (pt, data) { + if (this._items.length) { + var map = this._map, + layerPoint = map.containerPointToLayerPoint(pt), + latlng = map.layerPointToLatLng(layerPoint), + event = L.extend(data || {}, {contextmenu: this}); + + this._showLocation = { + latlng: latlng, + layerPoint: layerPoint, + containerPoint: pt + }; + + if (data && data.relatedTarget){ + this._showLocation.relatedTarget = data.relatedTarget; + } + + this._setPosition(pt); + + if (!this._visible) { + this._container.style.display = 'block'; + this._visible = true; + } + + this._map.fire('contextmenu.show', event); + } + }, + + _hide: function () { + if (this._visible) { + this._visible = false; + this._container.style.display = 'none'; + this._map.fire('contextmenu.hide', {contextmenu: this}); + } + }, + + _getIcon: function (options) { + return L.Browser.retina && options.retinaIcon || options.icon; + }, + + _getIconCls: function (options) { + return L.Browser.retina && options.retinaIconCls || options.iconCls; + }, + + _setPosition: function (pt) { + var mapSize = this._map.getSize(), + container = this._container, + containerSize = this._getElementSize(container), + anchor; + + if (this._map.options.contextmenuAnchor) { + anchor = L.point(this._map.options.contextmenuAnchor); + pt = pt.add(anchor); + } + + container._leaflet_pos = pt; + + if (pt.x + containerSize.x > mapSize.x) { + container.style.left = 'auto'; + container.style.right = Math.min(Math.max(mapSize.x - pt.x, 0), mapSize.x - containerSize.x - 1) + 'px'; + } else { + container.style.left = Math.max(pt.x, 0) + 'px'; + container.style.right = 'auto'; + } + + if (pt.y + containerSize.y > mapSize.y) { + container.style.top = 'auto'; + container.style.bottom = Math.min(Math.max(mapSize.y - pt.y, 0), mapSize.y - containerSize.y - 1) + 'px'; + } else { + container.style.top = Math.max(pt.y, 0) + 'px'; + container.style.bottom = 'auto'; + } + }, + + _getElementSize: function (el) { + var size = this._size, + initialDisplay = el.style.display; + + if (!size || this._sizeChanged) { + size = {}; + + el.style.left = '-999999px'; + el.style.right = 'auto'; + el.style.display = 'block'; + + size.x = el.offsetWidth; + size.y = el.offsetHeight; + + el.style.left = 'auto'; + el.style.display = initialDisplay; + + this._sizeChanged = false; + } + + return size; + }, + + _onKeyDown: function (e) { + var key = e.keyCode; + + // If ESC pressed and context menu is visible hide it + if (key === 27) { + this._hide(); + } + }, + + _onItemMouseOver: function (e) { + L.DomUtil.addClass(e.target || e.srcElement, 'over'); + }, + + _onItemMouseOut: function (e) { + L.DomUtil.removeClass(e.target || e.srcElement, 'over'); + } +}); + +L.Map.addInitHook('addHandler', 'contextmenu', L.Map.ContextMenu); +L.Mixin.ContextMenu = { + bindContextMenu: function (options) { + L.setOptions(this, options); + this._initContextMenu(); + + return this; + }, + + unbindContextMenu: function (){ + this.off('contextmenu', this._showContextMenu, this); + + return this; + }, + + addContextMenuItem: function (item) { + this.options.contextmenuItems.push(item); + }, + + removeContextMenuItemWithIndex: function (index) { + var items = []; + for (var i = 0; i < this.options.contextmenuItems.length; i++) { + if (this.options.contextmenuItems[i].index == index){ + items.push(i); + } + } + var elem = items.pop(); + while (elem !== undefined) { + this.options.contextmenuItems.splice(elem,1); + elem = items.pop(); + } + }, + + replaceContextMenuItem: function (item) { + this.removeContextMenuItemWithIndex(item.index); + this.addContextMenuItem(item); + }, + + _initContextMenu: function () { + this._items = []; + + this.on('contextmenu', this._showContextMenu, this); + }, + + _showContextMenu: function (e) { + var itemOptions, + data, pt, i, l; + + if (this._map.contextmenu) { + data = L.extend({relatedTarget: this}, e); + + pt = this._map.mouseEventToContainerPoint(e.originalEvent); + + if (!this.options.contextmenuInheritItems) { + this._map.contextmenu.hideAllItems(); + } + + for (i = 0, l = this.options.contextmenuItems.length; i < l; i++) { + itemOptions = this.options.contextmenuItems[i]; + this._items.push(this._map.contextmenu.insertItem(itemOptions, itemOptions.index)); + } + + this._map.once('contextmenu.hide', this._hideContextMenu, this); + + this._map.contextmenu.showAt(pt, data); + } + }, + + _hideContextMenu: function () { + var i, l; + + for (i = 0, l = this._items.length; i < l; i++) { + this._map.contextmenu.removeItem(this._items[i]); + } + this._items.length = 0; + + if (!this.options.contextmenuInheritItems) { + this._map.contextmenu.showAllItems(); + } + } +}; + +var classes = [L.Marker, L.Path], + defaultOptions = { + contextmenu: false, + contextmenuItems: [], + contextmenuInheritItems: true + }, + cls, i, l; + +for (i = 0, l = classes.length; i < l; i++) { + cls = classes[i]; + + // L.Class should probably provide an empty options hash, as it does not test + // for it here and add if needed + if (!cls.prototype.options) { + cls.prototype.options = defaultOptions; + } else { + cls.mergeOptions(defaultOptions); + } + + cls.addInitHook(function () { + if (this.options.contextmenu) { + this._initContextMenu(); + } + }); + + cls.include(L.Mixin.ContextMenu); +} +return L.Map.ContextMenu; +});