Clirio Cloud
A web-based collection of tools for managing workspaces in Clirio View. Includes a user dashboard, workspace and observation creation tools, and interactive Bing maps.
Overview
Clirio Cloud is a collection of tools that have been growing over time. These tools are provided to users of the Clirio Suite on a website developed with Blazor. Clirio builds solutions that help users inspect, document, and collaborate in 3D and XR on geospatial workspaces.
One of the main goals of Clirio Cloud is to complement the upload and organizational capabilities of the iOS Clirio apps, which serve as the primary capture platform. While a user can easily capture and upload 3D photogrammetry scans with Clirio's iOS apps, they might also want to upload other 3D models, for which Clirio Cloud provides an interface. Other workspace tools are upload and management of custom basemaps, upload of boreholes, guest invitations, point of interest creation, and workspace deletion.
Furthermore, Clirio Cloud provides an analytics dashboard to users, as well as an easy way to access the Clirio knowledge base and training videos. Finally, there also are subscription and account management tools.
Development
The website was initially set up by Timothy Thibault using Blazor, to try using a web framework that has C# as its programming language, which would be an advantage considering we also used C# for Unity development. After setting up authentication, subscription management, and workspace listing with metadata, development was mostly handed off to me.
After adding all of the workspace management features such as 3D model upload and user invitations, I started working on another set of stories revolving around the dashboard. I partnered up with Jordan Wischmann, who designed mockups, which I then implemented.
Learning
Code Sample
An example of data binding between the form and data model for the Create POI page.
<!-- CODE SNIPPET, NOT THE FULL FILE --><EditForm EditContext="@EC" class="w-100 flex flex-column align-items-start" OnSubmit="@HandleCreatePoiSubmit"> <DataAnnotationsValidator /> <div class="font-bold text-xl mt-3 mb-6">POI Details</div> <div class="form-group mb-3 w-100"> <label for="poiCreateDisplayName">Name:</label> <InputTextOnInput id="poiCreateDisplayName" name="poiCreateDisplayName" @bind-Value="poi.DisplayName" class="form-control" /> <ValidationMessage For="() => poi.DisplayName" /> </div> <div class="form-group mb-3 w-100"> <label for="poiCreateDescription">Description:</label> <InputTextArea id="poiCreateDescription" name="poiCreateDescription" @bind-Value="poi.Description" class="form-control" /> </div> <div class="flex flex-col w-100 lg:flex-row lg:gap-4"> <div class="form-group mb-3 w-100"> <label for="poiCreateLocationLatitude">Latitude:</label> <InputNumber id="poiCreateLocationLatitude" name="poiCreateLocationLatitude" @bind-Value="poi.Latitude" class="form-control" step="any" /> <ValidationMessage For="() => poi.Latitude" /> </div> <div class="form-group mb-3 w-100"> <label for="poiCreateLocationLongitude">Longitude:</label> <InputNumber id="poiCreateLocationLongitude" name="poiCreateLocationLongitude" @bind-Value="poi.Longitude" class="form-control" step="any" /> <ValidationMessage For="() => poi.Longitude" /> </div> <div class="form-group mb-3 w-100"> <label for="poiCreateLocationAltitude">Altitude:</label> <div class="input-group"> <InputNumber id="poiCreateLocationAltitude" name="poiCreateLocationAltitude" @bind-Value="poi.Altitude" class="form-control" step="any" /> <div class="input-group-append"> <span class="input-group-text">m</span> </div> </div> <ValidationMessage For="() => poi.Altitude" /> </div> </div> <!-- Map + Submit / Cancel btns --> <div id="map" style="@(!bingMapLoaded ? "height:0px;width:100%;position:relative;" : "height:500px;width:100%;position:relative;")" class="cursor-pointer mb-2"> </div> <div class="text-gray-500">Click on map to place cursor in your desired location or enter coordinates manually, including altitude (in meters). Click and hold to drag map. To switch map views, select icon in top right corner of map.</div> <div class="flex gap-4 mt-2"> <button @onclick="@(e => CancelButtonClicked())" class="btn text-white bg-red-500 hover:bg-red-600"> Cancel </button> <button type="submit" id="submitButton" class="@(EC.Validate() ? "btn text-white bg-clirio-500 hover:bg-clirio-600" : "btn text-white bg-gray-500 hover:bg-gray-600"))"> Create POI </button> </div></EditForm>@code { // SOME CODE OMITTED // [Parameter] public string? workspaceId { get; set; } [Parameter] public string? workspaceName { get; set; } private EditContext EC { get; set; } private static POICreate poi = new(); private HttpClient? workspaceClient; private bool processingRequest = false; protected override async Task OnInitializedAsync() { EC = new EditContext(poi); poi.WorkspaceId = workspaceId; poi.POIDateTime = DateTime.UtcNow; await base.OnInitializedAsync(); workspaceClient = HttpClientFactory.CreateClient("WorkspaceApi"); workspaceClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true }; LoadWorkspace(); } private async Task CreatePoi() { try { processingRequest = true; Console.WriteLine($"CreatePoi()"); string serialized = JsonConvert.SerializeObject(poi); await workspaceClient.PostAsJsonAsync<POICreate>("POI", poi); processingRequest = false; CancelButtonClicked(); } catch (Exception ex) { Console.WriteLine(ex); processingRequest = false; StateHasChanged(); return; } }}namespace Sitescan.Models{ public class POICreate { [Required] public string WorkspaceId { get; set; } [Required(ErrorMessage = "Name is required.")] [MinLength(1)] public string DisplayName { get; set; } public string Description { get; set; } [Required(ErrorMessage = "Altitude is required.")] public double? Altitude { get; set; } [Required(ErrorMessage = "Latitude is required.")] [Range(-90, 90, ErrorMessage = "Latitude is required, and must be between -90 and 90.")] public double? Latitude { get; set; } [Required(ErrorMessage = "Longitude is required.")] [Range(-180, 180, ErrorMessage = "Longitude is required, and must be between -180 and 180.")] public double? Longitude { get; set; } [Required] public DateTime POIDateTime { get; set; } }}