From 1c14274cdbdd9dbe940742d5116be5b1e32e9dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20J=C3=B6rg?= Date: Thu, 26 Jun 2025 14:09:56 +0200 Subject: [PATCH] feat: Add Initial Plume UI --- .devcontainer/Dockerfile | 4 +- README.md | 10 +- src/Atlantis/.build/Build.fs | 6 +- src/Atlantis/Dockerfile | 5 +- src/Atlantis/src/Client/Lib/Remoting.fs | 8 +- src/Atlantis/src/Client/Lib/Types.fs | 72 +- src/Atlantis/src/Client/Mapster/Drifters.fs | 4 +- src/Atlantis/src/Client/Mapster/Mapster.fs | 11 +- .../src/Client/Mapster/Mapster.fsproj | 1 + src/Atlantis/src/Client/Mapster/Model.fs | 19 +- src/Atlantis/src/Client/Mapster/Navigation.fs | 55 +- src/Atlantis/src/Client/Mapster/Plume.fs | 1006 +++++++++++++++++ src/Atlantis/src/Server/Api.fs | 80 ++ src/Atlantis/src/Server/Atlantis.fsi | 16 + src/Atlantis/src/Server/Hipster/PlumeActor.fs | 10 +- src/Atlantis/src/Server/Server.fsproj | 8 +- src/Atlantis/vite.config.js | 1 + src/Interfaces/Atlantis/Api.fs | 8 +- src/Interfaces/Hipster/Actors.fs | 13 +- src/Interfaces/Hipster/Job.fs | 47 +- 20 files changed, 1310 insertions(+), 74 deletions(-) create mode 100644 src/Atlantis/src/Client/Mapster/Plume.fs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 9ec243d4..56aebcc8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0 +FROM mcr.microsoft.com/dotnet/sdk:9.0.301 # Bun version ARG BUN_INSTALL=/usr/local @@ -26,4 +26,4 @@ ENV PATH="/root/.dotnet/tools:${PATH}" # Copy endpoint specific user settings into container to specify # .NET Core should be used as the runtime. -COPY settings.vscode.json /root/.vscode-remote/data/Machine/settings.json \ No newline at end of file +COPY settings.vscode.json /root/.vscode-remote/data/Machine/settings.json diff --git a/README.md b/README.md index 63858a30..c4c3b605 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Now, we should be able to `restore`: ```sh dotnet tool restore -dotnet restore +dotnet restore Poseidon.slnx ``` ### Mkcert @@ -163,3 +163,11 @@ In order for your browser to allow you to access the web application, you must a 2. Click on _"View Certificates",_ then _"Import..."_ in the _"Authorities"_ tab. 3. Select the root certificate; `~/.vite-plugin-mkcert/certs/rootCA.pem`. - Make sure to check _"This certificate can identify websites"._ + +### Add `user` to OpenFGA + +Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA. + +### CORS for Sorcerer + +Add the `url` of your instance to the CORS list of Sorcerer [here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52). diff --git a/src/Atlantis/.build/Build.fs b/src/Atlantis/.build/Build.fs index 8f547c93..755924ef 100644 --- a/src/Atlantis/.build/Build.fs +++ b/src/Atlantis/.build/Build.fs @@ -26,13 +26,13 @@ let fableWatch = $"fable watch -e .jsx -o build --run {vite}" Target.create "Clean" (fun _ -> Shell.cleanDir distPath) Target.create "Bundle" (fun _ -> - [ "server", dotnet $"publish -tl -c Release -o {distPath}" serverPath + [ "server", dotnet $"build -tl -c Release -o {distPath}" serverPath "client", dotnet (fable "-m production") clientPath ] |> runParallel ) Target.create "BundleDebug" (fun _ -> - [ "server", dotnet $"publish -tl -c Debug -o {distPath}" serverPath + [ "server", dotnet $"build -tl -c Debug --no-restore -o {distPath}" serverPath "client", dotnet (fable "-m development --minify false --sourcemap true") clientPath ] |> runParallel ) @@ -87,4 +87,4 @@ let dependencies = [ ] [] -let main args = runOrDefault args \ No newline at end of file +let main args = runOrDefault args diff --git a/src/Atlantis/Dockerfile b/src/Atlantis/Dockerfile index 98315eef..bca0e562 100644 --- a/src/Atlantis/Dockerfile +++ b/src/Atlantis/Dockerfile @@ -1,4 +1,5 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 +FROM mcr.microsoft.com/dotnet/aspnet:9.0.6 + RUN apt-get update \ && apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev @@ -11,4 +12,4 @@ ENV SERVER_CONTENT_ROOT=/app/public COPY dist/ /app WORKDIR /app -CMD [ "dotnet", "/app/Server.dll" ] \ No newline at end of file +CMD [ "dotnet", "/app/Server.dll" ] diff --git a/src/Atlantis/src/Client/Lib/Remoting.fs b/src/Atlantis/src/Client/Lib/Remoting.fs index 749cd8cb..5c251385 100644 --- a/src/Atlantis/src/Client/Lib/Remoting.fs +++ b/src/Atlantis/src/Client/Lib/Remoting.fs @@ -161,4 +161,10 @@ let cropStatsApi url = |> Remoting.withBaseUrl url |> Remoting.withRouteBuilder Api.Crop.routeBuilder |> Remoting.withBinarySerialization - |> Remoting.buildProxy \ No newline at end of file + |> Remoting.buildProxy + +let plumeApi () = + Remoting.createApi () + |> Remoting.withCredentials true + |> Remoting.withRouteBuilder Api.authorizedRouteBuilder + |> Remoting.buildProxy diff --git a/src/Atlantis/src/Client/Lib/Types.fs b/src/Atlantis/src/Client/Lib/Types.fs index 1024d445..03a900c8 100644 --- a/src/Atlantis/src/Client/Lib/Types.fs +++ b/src/Atlantis/src/Client/Lib/Types.fs @@ -606,9 +606,77 @@ type SimArchive = } +type PlumeSimModel = { + name: string + fvcom: System.Guid + start: DateTime + stop: DateTime + timeIdx: int +} with + static member empty = { + name = "" + fvcom = System.Guid.Empty + start = DateTime.Now + stop = DateTime.Now.AddDays(4) + timeIdx = 0 + } + +type PlumeReleaseSite = { + position: float * float + radius: float + theta: float + depth: float + // ddepth: float +} with + static member empty = { + position = 0.0, 0.0 + radius = 1.0 + theta = 10.0 + depth = -20.0 + // ddepth = 10.0 + } + +type PlumeGroupModel = { + name: string option + sites: PlumeReleaseSite list +} with + static member empty i = { + name = $"Group-{i}" |> Some + sites = List.Empty + } + +type PlumeTraits = { + temp: float + salt: float + transport: float +} with + static member empty = { + temp = 0.0 + salt = 0.0 + transport = 0.0 + } + +type PlumeType = + | DefaultPlume + member this.ToLabel() = + match this with + | DefaultPlume -> "Plume" + +type SimControlKind = + | Drifters of SimType + | Plume of PlumeType + member this.ToLabel() = + match this with + | Drifters simType -> simType.ToLabel () + | Plume plumeType -> plumeType.ToLabel () + member this.simTypeOpt = + match this with + | Drifters simType -> Some simType + | Plume plumeType -> None + type SideNavMode = | OceanControls - | SimControls of SimType + | SimControls of SimControlKind | AnalysisControls of SimArchive | ColorControls | LayerControls @@ -619,7 +687,7 @@ with member this.ToLabel () = match this with | OceanControls -> "Ocean controls" - | SimControls simType -> $"Simulation controls {simType.ToLabel ()}" + | SimControls simControlKind -> $"Simulation controls {simControlKind.ToLabel ()}" | AnalysisControls _ -> "Analysis controls" | ColorControls -> "Color controls" | LayerControls -> "Layer controls" diff --git a/src/Atlantis/src/Client/Mapster/Drifters.fs b/src/Atlantis/src/Client/Mapster/Drifters.fs index 22e85b83..74c3b48e 100644 --- a/src/Atlantis/src/Client/Mapster/Drifters.fs +++ b/src/Atlantis/src/Client/Mapster/Drifters.fs @@ -214,12 +214,12 @@ let fetchDrifters (api: ArchivesApi) (aid: Guid) : SimArchive [] JS.Promise = |> Array.map (fun (d, n, t) -> let reverse = match d.archiveType with - | Drifters (_, DriftersFormat.Particle) -> + | ArchiveType.Drifters (_, DriftersFormat.Particle) -> decodeDriftersInput d |> Option.map _.simulation.reverse |> Option.flatten |> Option.defaultValue false - | Drifters _ -> + | ArchiveType.Drifters _ -> decodePostdriftInput d |> Option.map _.analysis.reverse |> Option.flatten diff --git a/src/Atlantis/src/Client/Mapster/Mapster.fs b/src/Atlantis/src/Client/Mapster/Mapster.fs index a03e6ab8..97eb9132 100644 --- a/src/Atlantis/src/Client/Mapster/Mapster.fs +++ b/src/Atlantis/src/Client/Mapster/Mapster.fs @@ -712,7 +712,7 @@ let update cmd model = | _ -> TransportSim let m = { model with - sideNavMode = SideNavMode.SimControls kind + sideNavMode = SideNavMode.SimControls (Drifters kind) mode = Mode.Simulation Placing } @@ -1561,9 +1561,12 @@ let update cmd model = let n' = if n = abs(model.inboxUnread) then -model.inboxUnread else n // console.log $"unread: {n'}" { model with inboxUnread = n' }, Cmd.none - | SelectPlume id -> + | SetPlumeModel modelOpt -> + console.debug("[Mapster] SetPlumeModel:", modelOpt) + { model with plumeModelOpt = modelOpt }, Cmd.none + | SetSelectedPlume id -> console.log $"Selected plume: {id}" - { model with plume = id }, Cmd.none + { model with selectedPlume = Some id }, Cmd.none | HubMsg msg -> { model with hubAction = Some msg }, Cmd.none | Noop _ -> model, Cmd.none @@ -2042,7 +2045,7 @@ let MapAppElement () = let selectInboxItem (id, type': MessageType) = Hub.Action.Inbox (Hub.InboxMsg.MarkRead id) |> (HubMsg >> dispatch) match type' with - | MessageType.Plume -> SelectPlume (Some id) |> dispatch + | MessageType.Plume -> SetSelectedPlume id |> dispatch | MessageType.Drifters -> SetSelectedDrifter id |> dispatch | _ -> () diff --git a/src/Atlantis/src/Client/Mapster/Mapster.fsproj b/src/Atlantis/src/Client/Mapster/Mapster.fsproj index f84be205..00781b93 100644 --- a/src/Atlantis/src/Client/Mapster/Mapster.fsproj +++ b/src/Atlantis/src/Client/Mapster/Mapster.fsproj @@ -20,6 +20,7 @@ + diff --git a/src/Atlantis/src/Client/Mapster/Model.fs b/src/Atlantis/src/Client/Mapster/Model.fs index 7c6429d5..e2c38e35 100644 --- a/src/Atlantis/src/Client/Mapster/Model.fs +++ b/src/Atlantis/src/Client/Mapster/Model.fs @@ -224,6 +224,16 @@ type PostdriftModel = { openLayersMap: OlMap } +type PlumeModel = { + kind: PlumeType + simStarted: bool * int option + simulation: PlumeSimModel + sites: PlumeReleaseSite list + traits: PlumeTraits + selectedSite: (int * PlumeReleaseSite) option + openLayersMap: OlMap +} + type ContourData = (float*float)[][] type ContourModel = { kind: ContourKind @@ -334,7 +344,8 @@ type Model = { identity: Api.UserIdentity inboxUnread: int hubAction: Hub.Action option - plume: System.Guid option + plumeModelOpt: PlumeModel option + selectedPlume: ArchiveId option infectionNetwork: NetworkState } with @@ -399,7 +410,8 @@ with identity = Api.UserIdentity.empty inboxUnread = 0 hubAction = None - plume = None + plumeModelOpt = None + selectedPlume = None infectionNetwork = NetworkState.empty } @@ -504,7 +516,8 @@ type Msg = | DeleteArchive of System.Guid | SetIdentity of Api.UserIdentity | SetUnread of int - | SelectPlume of System.Guid option + | SetPlumeModel of PlumeModel option + | SetSelectedPlume of System.Guid | HubMsg of Hub.Action | Noop of unit diff --git a/src/Atlantis/src/Client/Mapster/Navigation.fs b/src/Atlantis/src/Client/Mapster/Navigation.fs index 19e5b497..6052de7b 100644 --- a/src/Atlantis/src/Client/Mapster/Navigation.fs +++ b/src/Atlantis/src/Client/Mapster/Navigation.fs @@ -140,6 +140,8 @@ let simAccordion (dispatch: Msg -> unit) model = // let canSubmit = model.simPolicies |> Array.fold (fun a x -> a || x.IsAllowed()) false console.debug $"policies: %A{model.simPolicies}" let disabled = model.archive.id = Guid.Empty + // TODO(mrtz): Set an actual condition on plumes + let disabledPlume = false let disabledTransport = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false) let disabledDeposition = model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false) let chooseMode kind _ = @@ -178,38 +180,45 @@ let simAccordion (dispatch: Msg -> unit) model = Transport Salmon lice Virus Deposition Water contact + + Plume + """ @@ -572,8 +581,13 @@ let sideNav (dispatch: Msg -> unit) (model: Model) (navMode: SideNavMode) = let simSelector (dispatch: Msg -> unit) (model: Model) = match model.mode with | Mode.Simulation Placing -> - console.log $"simControls : {kind}" - Drifters.simulationControls kind dispatch model + match kind.simTypeOpt with + | Some simType -> + console.log $"drifter simControls : {kind}" + Drifters.simulationControls simType dispatch model + | None -> + console.log $"plume simControls : {kind}" + Plume.simulationControls DefaultPlume dispatch model | Mode.Moot | Mode.Ocean | Mode.Stats _ @@ -731,22 +745,21 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch = | Mode.Ocean -> () | _ -> SetMode Mode.Ocean |> dispatch - let selectCompute (driftersModelOpt: DrifterModel option) ev = + let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) ev = if canSubmit then - match driftersModelOpt with - | Some d -> - console.debug($"We already have an ongoing sim : {d.simulation.kind}") - d.simulation.kind - |> SimControls - |> SetSideNavMode - |> dispatch + match driftersModelOpt, plumeModelOpt with + | Some d, _ -> + console.debug ($"We already have an ongoing driftser sim : {d.simulation.kind}") + (Drifters d.simulation.kind) |> SimControls |> SetSideNavMode |> dispatch - SimMode.Placing - |> Mode.Simulation - |> SetMode - |> dispatch + SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch + | None, Some p -> + console.debug ($"We already have an ongoing plume sim") + (Plume p.kind) |> SimControls |> SetSideNavMode |> dispatch + + SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch | _ -> - SimType.TransportSim + (Drifters SimType.TransportSim) |> SimControls |> SetSideNavMode |> dispatch @@ -804,7 +817,7 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch = Compute
-
+
{if model.drifterModelOpt.IsSome then statusIcon else Lit.nothing}
diff --git a/src/Atlantis/src/Client/Mapster/Plume.fs b/src/Atlantis/src/Client/Mapster/Plume.fs new file mode 100644 index 00000000..38613a2b --- /dev/null +++ b/src/Atlantis/src/Client/Mapster/Plume.fs @@ -0,0 +1,1006 @@ +module Plume + +open System +open Browser +open Feliz +open Fable.Core +open Fable.Core.JsInterop +open Fable.OpenLayers +open Hipster.Actors +open Lit +open Lit.Elmish +open Maps +open Microsoft.FSharp.Collections +open Remoting + +open Atlantis.Types +open Atlantis.Shared.Notification +open Utils + +open Hipster.Job +open Model +open Layers + +// +// === Helpers === +// + +let maxRadius = 10000.0 + +let inline toReact (el: JSX.Element) : Fable.React.ReactElement = unbox el + +let private formatDigits n m = + {| minimumFractionDigits = n; maximumFractionDigits = m |} |> JS.JSON.stringify +let private formatPercent = + {| + style = "unit" + unit = "%" + minimumFractionDigits = 1 + maximumFractionDigits = 1 + |} + |> JS.JSON.stringify +let private tryParseFloat (s: string) : float option = + s + |> Double.TryParse + |> function + | true, f -> Some f + | _ -> None + +// +// === Elmish === +// + +type SiteIdx = int + +type private PlumeMsg = + | SetRelease of DateTime * DateTime + | SetReleaseSites of PlumeReleaseSite list + | AddReleaseSites of PlumeReleaseSite list + | RemoveReleaseSite of SiteIdx + | UpdateReleaseSite of SiteIdx * (PlumeReleaseSite -> PlumeReleaseSite) + | SetTraits of PlumeTraits + | SetSimModel of PlumeSimModel + | SetSimStarted of bool * int option + | SelectSite of SiteIdx option + /// Simple msg for ensuring that you're not working with a stale model + | ResetModel of PlumeModel + | Noop of unit + +let simStatusMessage (job: JobInfo) = + match job.status with + | JobStatus.New -> Note.info "New simulation" + | JobStatus.Waiting -> Note.info "Waiting..." + | JobStatus.Running -> Note.info "Running..." + | JobStatus.Completed -> Note.info "Simulation finished" + | JobStatus.Unknown -> Note.warn "Job status is unknown" + | _ (*Failed*) -> Note.error "Simulation failed" + +/// NOTE(mrtz): Not in use +/// line : Lat, Lon, Radius, Depth, Theta; +let private readSiteLine tryFence (defaultSite: PlumeReleaseSite) (line: string) : PlumeReleaseSite option = + line.Split ([| '\t'; ' '; ','; ',' |]) + |> Array.filter ((<>) "") + |> function + // yes, lon/lat is switching here + | [| lat; lon; r; d; t |] -> + tryParseFloat lon, tryParseFloat lat, tryParseFloat r, tryParseFloat d, tryParseFloat t + | [| lat; lon |] -> + tryParseFloat lat, + tryParseFloat lon, + Some defaultSite.radius, + Some defaultSite.depth, + Some defaultSite.theta + | _ -> None, None, None, None, None + |> function + | Some lon, Some lat, Some r, Some d, Some t -> + { + position = (lon, lat) |> toEpsg3857' + radius = r + depth = -d + theta = t + // ddepth = dd + } + |> tryFence + | _ -> None + +/// +/// Clears all release features, and add all sites as features to the map +/// +/// +/// +let updateSelectedReleaseSite (siteOpt: (int*PlumeReleaseSite) option, map) = + map + |> updateBaseLayer MapLayer.SelectedReleaseGroup (fun baseLayer -> + let layer = baseLayer :?> VectorLayer + let source = layer.getSource () :?> VectorSource + source.clear() + + match siteOpt with + | Some (sIdx, site) -> + let lat = site.position |> toWgs84' |> snd + let r' = site.radius * mercatorScaleFactor lat + let p' = site.position |> posToCoord + let circle = Geometry.circle p' r' GeometryLayout.XY + let feature = + Feature.feature [ + feature.geometryOrProperties circle + ] + feature.setId(1000*1 + sIdx) + + source.addFeature(feature) + | _ -> () + ) + +let private createModifyInteraction (name: string) (model: Model) = + // NOTE(simkir): get a OpenLayers interaction from the map, so that the modify interaction will only work on the + // currently selected feature. + let selectInteraction = getInteractionByName model.map name |> Option.get :?> Select + // Interactions + let modify = + Interaction.modify [ + modify.features (selectInteraction.getFeatures ()) // Only allow modifying selected features + ] + modify.set ("name", "modifyInteraction") + + modify + +let private update (msg: PlumeMsg) (model: PlumeModel) = + match msg with + | SetRelease (s, e) -> + console.debug ("[Plume] SetRelease msg :", s) + { model with simulation.start = s; simulation.stop = e }, Elmish.Cmd.none + | SetReleaseSites s -> + console.debug ($"[Plume] SetReleaseSite msg :%A{s}") + let sIdxOpt = model.selectedSite |> Option.map fst + { model with sites = s }, Elmish.Cmd.batch [ Elmish.Cmd.ofMsg (SelectSite sIdxOpt) ] + | AddReleaseSites newSites -> + console.debug ($"[Plume] AddReleaseSite msg : {newSites.Length}") + let setSitesMsg = model.sites |> List.append newSites |> SetReleaseSites + model, Elmish.Cmd.ofMsg setSitesMsg + | RemoveReleaseSite sIdx -> + console.debug ($"[Plume] RemoveReleaseSite msg :%d{sIdx}") + let removeSitesMsg = + model.sites + |> List.mapi (fun i s -> (i <> sIdx), s) + |> List.filter fst + |> List.map snd + |> SetReleaseSites + model, Elmish.Cmd.ofMsg removeSitesMsg + | UpdateReleaseSite (sIdx, f) -> + console.debug ($"[Plume] UpdateReleaseSite msg :${sIdx}") + let updateSitesMsg = + model.sites + |> List.mapi (fun i s -> if i = sIdx then f s else s) + |> SetReleaseSites + model, Elmish.Cmd.ofMsg updateSitesMsg + | SelectSite sIdxOpt -> + console.debug ($"[Plume] SelectSite msg :%A{sIdxOpt}") + // NOTE: We can deselect a site, so the id must be an option + let siteOpt = + sIdxOpt + |> Option.bind (fun siteId -> + model.sites + |> List.tryItem siteId + |> Option.map (fun site -> siteId, site) + ) + + console.debug ($"[Plume] SelectSite msg not opt :%A{snd (siteOpt.Value)}") + siteOpt + |> Option.map snd + |> Option.iter (fun site -> + let zoom = if site.radius > 75.0 then 12.0 else 16.0 + let coord = site.position |> posToCoord + flyTo model.openLayersMap zoom coord) + + let m = { model with selectedSite = siteOpt } + m, Elmish.Cmd.OfFunc.perform updateSelectedReleaseSite (m.selectedSite, m.openLayersMap) PlumeMsg.Noop + | SetTraits t -> + console.debug ("[Plume] SetTraits msg:", t) + { model with traits = t }, Elmish.Cmd.none + | SetSimModel s -> + console.debug ("[Plume] SetSimModel msg:", s) + { model with simulation = s }, Elmish.Cmd.none + | PlumeMsg.SetSimStarted (started, jobIdOpt) -> {model with simStarted = (started, jobIdOpt)}, Elmish.Cmd.none + | PlumeMsg.ResetModel m -> { m with simulation.name = model.simulation.name }, Elmish.Cmd.none + | PlumeMsg.Noop () -> model, Elmish.Cmd.none + +// +// === Views and components === +// + +[] +let placingToggleButton (disabled: bool) (map: OlMap) (onPlace: Coordinate -> unit) = + let placing, setPlacing = Hook.useState false + let mapClickKey = Hook.useRef () + + let releaseClickHandler (e: Event.MapBrowserEvent) = + onPlace e.coordinate + setPlacing false + + // NOTE(simkir): If this button gets unmounted, the placing effect listener does not run its dispose function, + // therefore we have to here save the key, and dispose it on unmount. Terrible, I know :( + Hook.useEffectOnce (fun () -> + Hook.createDisposable (fun () -> + let elem = map.getTargetElement () + elem?style?cursor <- "" + mapClickKey.contents |> Option.iter Observable.unByKey)) + + Hook.useEffectOnChange (placing, crossHairSelect map mapClickKey releaseClickHandler) + + html + $""" + setPlacing (not placing))} + > + + Add by click + + """ + +[] +let private releaseSitesControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeModel) = + let defaultSite, setDefaultSite = Hook.useState PlumeReleaseSite.empty + console.debug ("[Plume.SimControls] defaultSite :", defaultSite) + + // TODO(mrtz): Add fencing + let tryFence (site: PlumeReleaseSite) : PlumeReleaseSite option = Some site + // match xmodel'.fence with + // | None -> Some site + // | Some pts -> + // let radius = sessionStorage["fence_radius"] |> float + // let coords' = toEpsg3857 pts[0] + // let radius' = radius * mercatorScaleFactor (snd pts[0]) + // let circle = Geometry.circle coords' radius' GeometryLayout.XY + // if circle.intersectsCoordinate (site.position |> posToCoord) then + // Some site + // else + // console.error("Trying to place release site outside of fencing radius") + // None + + let handleMapPlaceRelease (coords: Coordinate) = + console.debug ($"Click add site : %s{coords.ToString ()}") + + { defaultSite with position = coordToPos coords } + |> tryFence + |> Option.map (fun site -> + [ site ] |> PlumeMsg.AddReleaseSites |> dispatch' + ) + |> Option.defaultValue () + + // TODO(mrtz): Change for multiple sites + Some 0 + |> PlumeMsg.SelectSite + |> dispatch' + + let updateSelectedSite (updateFn: PlumeReleaseSite -> PlumeReleaseSite) : unit = + match xmodel'.selectedSite with + | Some (sIdx, _) -> (sIdx, updateFn) |> UpdateReleaseSite |> dispatch' + | _ -> defaultSite |> updateFn |> setDefaultSite + + let selectedSitePos = + xmodel'.selectedSite + |> Option.map (snd >> _.position) + |> Option.defaultValue defaultSite.position + |> toWgs84' + + let setRadius (v: float) = + console.debug ("[Plume.SimControls] setRadius :", v) + updateSelectedSite (fun site -> { site with radius = v }) + + let setDepth (v: int) = + console.debug ("[Plume.SimControls] setDepth :", v) + updateSelectedSite (fun site -> { site with depth = -v }) + + let setTheta (v: int) = + console.debug ("[Plume.SimControls] setTheta :", v) + updateSelectedSite (fun site -> { site with theta = v }) + + // let setDDepth (v: int) = + // console.debug("[Drifters.SimControls] setDDepth :", v) + // updateSelectedSite (fun site -> { site with ddepth = v } ) + + let radiusBox = + let radius = + xmodel'.selectedSite + |> Option.defaultValue (-1, defaultSite) + |> (snd >> _.radius) + + html + $""" + > setRadius)} + value={radius} + > + + """ + + + let depthBox = + let depth = + xmodel'.selectedSite + |> Option.defaultValue (-1, defaultSite) + |> (snd >> (fun s -> -s.depth)) + html + $""" + + """ + + let thetaBox = + let theta = + xmodel'.selectedSite + |> Option.defaultValue (-1, defaultSite) + |> (snd >> _.theta) + html + $""" + + """ + + let latitudeBox = + let latitude = snd selectedSitePos + html + $""" + + >> fun v -> + let newPos = toEpsg3857' (fst selectedSitePos, v) + updateSelectedSite (fun site -> { site with position = newPos }) + )} + > + + """ + + let longitudeBox = + let longitude = fst selectedSitePos + html + $""" + + + >> fun v -> + let newPos = toEpsg3857' (v, snd selectedSitePos) + updateSelectedSite (fun site -> { site with position = newPos }) + )} + > + + """ + + html + $""" +
+ {placingToggleButton xmodel'.selectedSite.IsSome xmodel'.openLayersMap handleMapPlaceRelease} +
+ +
+ +
+ + + + + Radius (m) + + {radiusBox} + + + + Depth (m) + + {depthBox} + + + + Angle (degrees) + + {thetaBox} + + + + + Latitude + + {latitudeBox} + + + + Longitude + + {longitudeBox} + + + + +
+ +
+ """ + +[] +let private plumeControls (dispatch': PlumeMsg -> unit) (xmodel': PlumeModel) = + html + $""" + + {releaseSitesControls dispatch' xmodel'} + + """ + +// +// TODO(simkir): do not send in the entire model. What is needed is: +// - archive +// - current frame +// - the current mode +// - the current drifters +// - The releases +// +/// Add OpenLayers circle interactions +/// Link: https://openlayers.org/en/latest/examples/draw-and-modify-features.html +[] +let simulationControls (plumeType: PlumeType) (dispatch: Msg -> unit) (model: Model) = + let archive = model.archive + let archiveStartUTC = archive.startTime.ToUniversalTime () + let archiveEndT = + archiveStartUTC.AddSeconds (archive.frames * archive.saveFreq |> float) + let simStartT = archiveStartUTC.AddSeconds (model.frame * archive.saveFreq |> float) + let submitted, setSubmitted = Hook.useState false + + // TODO: Move this to createNewModel? + // let inputOpt = + // model.selectedPlume + // |> Option.bind (fun archive -> + // tryStr archive.Archive.json + // |> Option.bind (fun json -> + // let inputRes : Result = + // json + // |> Thoth.Json.Decode.Auto.fromString + // + // match inputRes with + // | Result.Error err -> + // console.error("[Drifters.SimControls] failed decoding archive json:", archive.Archive.json) + // console.debug("[Drifters.SimControls] error:", err) + // None + // | Ok input -> + // Some input + // ) + // ) + + // NOTE: Start the model, but if we cloned a previous simulation, we must take care of a few things here + let createNewModel () : PlumeModel = + let iotrans s = { s with position = s.position |> toEpsg3857' } + + let simulation: PlumeSimModel = { + PlumeSimModel.empty with + fvcom = archive.id + // name = archive.name + "-plume" + start = simStartT + stop = simStartT.AddDays(1) + // timeIdx = 0 + } + // model. + // inputOpt + // |> Option.map (fun input -> + // let name = model.selectedDrifters |> Option.map (fun archive -> archive.Archive.name + "-clone") + // let timeSpan = input.simulation.duration |> TimeSpan.FromHours + // let saveFreq = input.simulation.saveFreq |> Option.defaultValue model.archive.saveFreq + // { PlumeSimModel.empty simType with + // fvcom = archive.id + // name = name |> Option.defaultValue "Plume" + // startTime = input.simulation.start |> Option.defaultValue simStartT + // stopTime = input.simulation.stop |> Option.defaultValue simStartT + // timeIdx = timeSpan.Days + // }) + // |> Option.defaultValue + // { PlumeSimModel.empty simType with + // fvcom = archive.id + // name = name |> Option.defaultValue "Plume" + // start = input.simulation.start |> Option.defaultValue simStartT + // stop = input.simulation.stop |> Option.defaultValue simStartT + // timeIdx = 0 + // } + + + let traits = PlumeTraits.empty + + // let selectedGroup = + // sites + // |> Map.tryFind 0 + // |> Option.map (fun g -> (0, g)) + + console.debug ("[Plume.SimControls] initialModel release:", traits, "sim:", simulation) + // NOTE: If the top-level map has saved a model, us it instead + { + // fence = model.archive.polygon + kind = DefaultPlume + simStarted = false, None + sites = [] + // simStarted = false, None + simulation = simulation + // release = release + traits = traits + // sedimentation = SedimentationModel.defaults + selectedSite = None + // tmpDrifter = simulation |> SimArchive.Create |> Some + openLayersMap = model.map + } + let initialModel () = + let xmodel' = + model.plumeModelOpt |> Option.defaultWith (fun () -> createNewModel ()) + xmodel', Elmish.Cmd.none + + let xmodel', dispatch' = Hook.useElmish (initialModel, update) + let modelRef = Hook.useRef (Some xmodel') + + modelRef.contents |> SetPlumeModel |> dispatch + + /////////////// + // META DATA // + /////////////// + + let setName (s: string) = + SetSimModel { xmodel'.simulation with name = s } |> dispatch' + + let setStartDate (str: string) = + let startDate = DateTime.Parse (str) + let startDate' = simStartT + + SetSimModel { xmodel'.simulation with start = startDate } |> dispatch' + + // let setStopDate (str: string) = + // let stopDate = DateTime.Parse(str) + // SetSimModel { xmodel'.simulation with stop = stopDate } + // |> dispatch' + + // TODO: Changing this should update the top-level release, as we want to place the simulation on the timeline + let setEndT (e: Browser.Types.Event) = + let nDays: float = !!e.target.Value + let duration = nDays |> TimeSpan.FromDays + console.debug ($"Update simulation days: dur = {duration} ({nDays})") + let stopDate = xmodel'.simulation.start.AddDays nDays + + SetSimModel { xmodel'.simulation with stop = stopDate } |> dispatch' + + let maxDurationH = + let buffer = 1.0 + (archiveEndT - simStartT).TotalHours - buffer + + let simDays = (xmodel'.simulation.stop - xmodel'.simulation.start).TotalDays + + // let Picker(title: string, callback: unit -> unit) = + // JSX.jsx""" + // import {DatePicker,defaultTheme, Provider} from '@adobe/react-spectrum' + // + // + // + // """ + // |> toReact + // + // let litPicker f: TemplateResult = + // React.toLit Picker f + + let metaControls = + html + $""" +
+ + Name (required) + + + + +
+ + +
+ + Duration (days) + + +
+
+ """ + + + //////////////////// + // SUBMIT BUTTONS // + //////////////////// + + let submit _ = () + let submit _ = + if archive.id <> Guid.Empty && xmodel'.sites.Length > 0 then + let traits' = xmodel'.traits + let site' = + xmodel'.sites + |> List.tryHead + |> Option.defaultWith (fun () -> PlumeReleaseSite.empty) + let sim' = xmodel'.simulation + + let lat, long = site'.position |> toWgs84' + let latLong = { Lat = lat; Long = long } + + let input: PlumeJob = { + PlumeJob.empty with + name = sim'.name + fvcom = Guid.NewGuid() + pos = latLong + temp = traits'.temp + salt = traits'.salt + depth = site'.depth + theta = site'.theta + transport = traits'.transport + radius = site'.radius + start = sim'.start + stop = sim'.stop + } + + console.log $"\n-------------- Plume input ----------------" + console.log $"Simulation type : {sim'.name}" + console.log $"%A{input}" + + let api = plumeApi () + async { + let! job = api.startPlume input + setSubmitted true + + match job with + | None -> Note.failure "[Plume] Job submission failed" |> SetNotification |> dispatch + | Some j -> + // get new status (if queued or failed) + do! Async.Sleep 1000 + let! job' = api.getSimInfo j.jobId + let status = job' |> Option.map _.status |> Option.defaultValue JobStatus.Waiting + console.log $"[Plume.SimControls] Status is: {status}" + + // TODO(mrtz): Create tmpPlume later + // xmodel'.tmpDrifter + // |> Option.map (fun d -> + // { d with + // Archive = + // { d.Archive with + // archiveId = j.archiveId + // name = j.name + // } + // Status = status + // } + // ) + // |> SetTmpDrifter + // |> dispatch' + + // note: we get the running status message from the Inbox actor when the job starts + if status <> JobStatus.Running then + simStatusMessage { j with status = status } |> SetNotification |> dispatch + + if + status = JobStatus.Waiting + || status = JobStatus.Running + || status = JobStatus.Completed + then + PlumeMsg.SetSimStarted (true, Some j.jobId) |> dispatch' + } + |> Async.StartImmediate + + let reset (_: Browser.Types.Event) = + console.debug ("[Plume.SimControls] Reset sim") + clearFeatures model.map MapLayer.SelectedReleaseGroup + clearFeatures model.map MapLayer.UnselectedReleaseGroups + + createNewModel () |> PlumeMsg.ResetModel |> dispatch' + + let cancel (_: Browser.Types.Event) = + console.debug ("[Plume.SimControls] Cancel sim") + clearFeatures model.map MapLayer.SelectedReleaseGroup + clearFeatures model.map MapLayer.UnselectedReleaseGroups + + modelRef.contents <- None + SetPlumeModel None |> dispatch + SetMode Mode.Moot |> dispatch + + + let submitButtons = + let noName = false + let noName = String.IsNullOrWhiteSpace (xmodel'.simulation.name) + let noSites = false + let noSites = xmodel'.sites.Length |> (=) 0 + + html + $""" +
+ + + Submit + + + Reset + + + Cancel + + +
+ """ + + // + // On mount and unmount + // + Hook.useEffectOnce (fun () -> + console.debug ("[Plume.SimControls] === mounting ===") + console.log $"Plume current site: {xmodel'.selectedSite}" + let selectInteraction = + getInteractionByName model.map "releaseSelect" |> Option.get :?> Select + + selectInteraction.on ( + "select", + fun (selectEvent: SelectEvent) -> + let selectTarget = selectEvent.target :?> Select + if Array.isEmpty selectEvent.selected then + None |> SelectSite |> dispatch' + else + let feature = selectEvent.selected[0] + let siteId = feature.getId () + + Some siteId |> SelectSite |> dispatch' + ) + + let releaseModify = createModifyInteraction "releaseSelect" model + releaseModify.on ( + "modifyend", + fun (e: ModifyEvent) -> + let features: Feature Collection.Collection = e.features + + let feature = features.item 0 + let fIdx = feature.getId () + let circle = feature.getGeometry () :?> Geometry.Circle + + let center' = circle.getCenter () |> coordToPos + let lat = center' |> toWgs84' |> snd + let radius' = circle.getRadius () / mercatorScaleFactor lat + + (fIdx, fun (site: PlumeReleaseSite) -> { site with radius = radius'; position = center' }) + |> UpdateReleaseSite + |> dispatch' + + None |> SelectSite |> dispatch' + ) + + model.map.addInteraction releaseModify + + Hook.createDisposable (fun () -> + console.log "Leaving simControls" + model.map.removeInteraction releaseModify |> ignore + + console.debug ("[Plume.SimControls] Plume model ref", modelRef) + modelRef.contents |> SetPlumeModel |> dispatch)) + + // + // Component changes + // + + Hook.useEffectOnChange ( + xmodel', + fun newPlumeModel -> + console.debug ("[Plume.SimControls] Plume model changed", newPlumeModel) + modelRef.contents <- Some newPlumeModel + console.debug ("[Plume.SimControls] Plume model ref", modelRef) + ) + + // TODO(mrtz): Update this later with a tmpPlume + // NOTE: This is what updates the timeline + // Hook.useEffectOnChange ( + // xmodel'.tmpDrifter, + // fun drifterOpt -> + // console.debug("[Drifters.SimControls] Change tmpDrifter, aka update top-level selected drifter", drifterOpt) + // drifterOpt + // |> SetSelectedSimArchive + // |> dispatch + // ) + + // Hook.useEffectOnChange ( + // xmodel'.simStarted, + // fun (updatedStarted, jobIdOpt) -> + // console.warn("Drifters model simStarted changed!", updatedStarted) + // if updatedStarted then + // if jobIdOpt.IsSome then + // console.log("sim started: setting up analysis") + // None + // |> SelectGroup + // |> dispatch' + + // let g : GroupType list = + // match simType with + // | DepositionSim -> + // // TODO: fix ugliness + // xmodel'.release.groups + // |> Map.values + // |> Seq.toList + // |> List.map (fun g -> + // g.sites + // |> List.mapi (fun i _ -> + // g.name + // |> Option.map (fun n -> n + $"-{i + 1}") + // |> Option.defaultValue $"Particle-{i + 1}") + // ) + // |> List.concat + // |> List.mapi (fun i n -> { idx = i; kind = Group i; name = n }) + // | _ -> + // xmodel'.release.groups + // |> Map.toList + // |> List.map (fun (i, g) -> (i, g.name |> Option.defaultValue $"Group-{i}")) + // |> List.map (fun (i, n) -> { idx = i; kind = Group i; name = n }) + + // let p : ParticleType list = + // xmodel'.traits + // |> Map.toList + // |> List.mapi (fun i (k, p) -> (k, p.name |> Option.defaultValue $"Particle-{i}")) + // |> List.mapi (fun i (k, n) -> { idx = i; kind = k; name = n }) + + // { model.particleFilter with + // fetched = true + // availableGroupTypes = g + // availableParticleTypes = p + // } + // |> Msg.SetParticleFilter + // |> dispatch + + // xmodel'.tmpDrifter + // |> Option.map (fun x -> { x with JobId = jobIdOpt }) + // |> Option.map (NewPostdriftAnalysis >> dispatch) + // |> Option.defaultValue () + // else + // console.log("sim started: resetting") + // // NOTE(simkir): Reset after we get the job status, so that we know whether the sim was + // // aborted or successfully started. + // Msg.SetCustomGrid None |> dispatch + // Msg.UnsetSelectedDrifter () |> dispatch + // Msg.SetDriftersModel None |> dispatch + // Msg.SetSideNavMode OceanControls |> dispatch + // modelRef.contents <- None + // ) + + + // Top-level model changes + + Hook.useEffectOnChange ( + model.frame, + fun _ -> + // NOTE(simkir): Only update the sim item when it is new, when it has been started, do not update + { xmodel'.simulation with start = simStartT } |> SetSimModel |> dispatch' + + // match xmodel'.tmpDrifter with + // | Some d when d.Status = JobStatus.New -> + // let d' = Some { d with Archive = { d.Archive with startTime = simStartT }; Reverse = xmodel'.simulation.reverse } + // SetTmpDrifter d' |> dispatch' + // | _ -> () + ) + + let measuresHeight = + tryGetElemRect "measures-controls" + |> Option.map _.height + |> Option.defaultValue 80 + + html + $""" +
+

{plumeType.ToLabel () + " Simulation"}

+
+ + {metaControls} + {match xmodel'.kind with + | DefaultPlume -> plumeControls dispatch' xmodel' + | _ -> Lit.nothing} +
+
+ {submitButtons} + """ diff --git a/src/Atlantis/src/Server/Api.fs b/src/Atlantis/src/Server/Api.fs index 1c0451e1..b4779426 100644 --- a/src/Atlantis/src/Server/Api.fs +++ b/src/Atlantis/src/Server/Api.fs @@ -475,6 +475,80 @@ module Handlers = |> Async.AwaitTask } + let private runPlume + (ctx: HttpContext) + (plumeJob: PlumeJob) + = + task { + let user = getUserName ctx + let group = getGroup ctx + + let id = ActorId user + + try + let proxy = ActorProxy.Create (id, "PlumeActor") + Log.Information ("runPlume: user {username}, fvcom={@fvcom}, name={@name}", user, plumeJob.fvcom, plumeJob.name) + + match! proxy.Submit plumeJob with + | Ok info -> + Log.Debug $"jobid is {info.jobId} ({info.archiveId})" + return Some info + | Error e -> + Log.Warning $"job submit failed: {e}" + + match SignalRHub.getHub ctx with + | Some h -> + do! h.Clients.User(user).Send (Hub.Response.Note (Note.failure $"Job submission failed: {e}")) + return None + | None -> + Log.Error "Could not get signalr hub context" + return None + with exn -> + Log.Error $"runPlume: {exn.Message}" + Log.Verbose $"runPlume: %A{exn}" + return None + } + |> Async.AwaitTask + + let private getPlumeSimInfo (ctx: HttpContext) (jobId: int) = + task { + let user = getUserName ctx + let id = ActorId user + + try + let proxy = ActorProxy.Create (id, "PlumeActor") + let! job = getJobInfo proxy jobId + return job + with exn -> + Log.Error $"[Plume] getSimInfo: {exn.Message}" + Log.Verbose $"[Plume] getSimInfo: %A{exn}" + return None + } + |> Async.AwaitTask + + let private getActivePlumeSims (ctx: HttpContext) (aId: Guid) = + task { + let user = getUserName ctx + let id = ActorId user + + try + let proxy = ActorProxy.Create (id, "PlumeActor") + let! jobs = proxy.GetActiveJobs aId + Log.Information ("[Plume] Active jobs for {username}@{aid}: {jobs}", user, aId, jobs.Length) + return jobs + with exn -> + Log.Error $"[Plume] getActiveSimulations: %s{exn.Message}" + Log.Verbose $"[Plume] getActiveSimulations: %A{exn}" + return [||] + } + |> Async.AwaitTask + + let plumeApi (ctx: HttpContext) : Api.Plume = { + startPlume = runPlume ctx + getActiveJobs = getActivePlumeSims ctx + getSimInfo = getPlumeSimInfo ctx + } + module Endpoints = let authEndpoints: HttpHandler = Remoting.createApi () @@ -504,4 +578,10 @@ module Endpoints = Remoting.createApi () |> Remoting.fromContext Handlers.inboxApi |> Remoting.withRouteBuilder Api.authorizedRouteBuilder + |> Remoting.buildHttpHandler + + let plumeEndpoints: HttpHandler = + Remoting.createApi () + |> Remoting.fromContext Handlers.plumeApi + |> Remoting.withRouteBuilder Api.authorizedRouteBuilder |> Remoting.buildHttpHandler \ No newline at end of file diff --git a/src/Atlantis/src/Server/Atlantis.fsi b/src/Atlantis/src/Server/Atlantis.fsi index 8d6a1fd3..d451ea93 100644 --- a/src/Atlantis/src/Server/Atlantis.fsi +++ b/src/Atlantis/src/Server/Atlantis.fsi @@ -424,6 +424,20 @@ module Handlers = val driftersApi: ctx: Microsoft.AspNetCore.Http.HttpContext -> Drifters val inboxApi: ctx: Microsoft.AspNetCore.Http.HttpContext -> Inbox + + val private runPlume: + ctx: Microsoft.AspNetCore.Http.HttpContext -> + plumeJob: Hipster.Actors.PlumeJob -> Async + + val private getPlumeSimInfo: + ctx: Microsoft.AspNetCore.Http.HttpContext -> + jobId: int -> Async + + val private getActivePlumeSims: + ctx: Microsoft.AspNetCore.Http.HttpContext -> + aId: System.Guid -> Async + + val plumeApi: ctx: Microsoft.AspNetCore.Http.HttpContext -> Plume module Endpoints = @@ -436,6 +450,8 @@ module Endpoints = val driftersEndpoints: Giraffe.Core.HttpHandler val inboxEndpoints: Giraffe.Core.HttpHandler + + val plumeEndpoints: Giraffe.Core.HttpHandler module Atlantis.Events diff --git a/src/Atlantis/src/Server/Hipster/PlumeActor.fs b/src/Atlantis/src/Server/Hipster/PlumeActor.fs index 8ea2f987..4c59dd72 100644 --- a/src/Atlantis/src/Server/Hipster/PlumeActor.fs +++ b/src/Atlantis/src/Server/Hipster/PlumeActor.fs @@ -26,8 +26,8 @@ type PlumeEnv = { member this.toJson() = [ "PLUME_ID", Encode.guid this.aid "PLUME_NAME", Encode.string this.job.name - "PLUME_LAT", Encode.tuple2 Encode.float Encode.float this.job.pos - "PLUME_LNG", Encode.tuple2 Encode.float Encode.float this.job.pos + "PLUME_LAT", Encode.float this.job.pos.Lat + "PLUME_LNG", Encode.float this.job.pos.Long "PLUME_TEMP", Encode.float this.job.temp "PLUME_SALT", Encode.float this.job.salt "PLUME_DEPTH", Encode.float this.job.depth @@ -128,10 +128,10 @@ type PlumeActor let! allowed = fga.checkRelation tuple if allowed then - Log.Debug $"DriftersActor.StartDrifters user validated: {this.myId}" + Log.Debug $"PlumeActor.StartPlume user validated: {this.myId}" - if this.validatePoints job.fvcom [ job.pos ] then - Log.Debug $"DriftersActor.StartDrifters release points validated: {this.myId}" + if this.validatePoints job.fvcom [ (job.pos.Lat, job.pos.Long) ] then + Log.Debug $"PlumeActor.StartPlumes release points validated: {this.myId}" return! submitJob () else let msg = "One or more release points outside boundary." diff --git a/src/Atlantis/src/Server/Server.fsproj b/src/Atlantis/src/Server/Server.fsproj index 014b24fa..e45bc3e6 100644 --- a/src/Atlantis/src/Server/Server.fsproj +++ b/src/Atlantis/src/Server/Server.fsproj @@ -4,6 +4,12 @@ Exe net9.0 true + true + [UNDEFINED] + true + true + + true 2.102.0 Server $(OtherFlags) --sig:Atlantis.fsi --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen --strict-indentation+ @@ -88,4 +94,4 @@ <_ContentIncludedByDefault Remove="Atlantis\obj\Release\net9.0\staticwebassets.publish.endpoints.json" /> <_ContentIncludedByDefault Remove="Atlantis\obj\Release\net9.0\staticwebassets.publish.json" /> - \ No newline at end of file + diff --git a/src/Atlantis/vite.config.js b/src/Atlantis/vite.config.js index 8e4395e1..e8962168 100644 --- a/src/Atlantis/vite.config.js +++ b/src/Atlantis/vite.config.js @@ -54,6 +54,7 @@ export default defineConfig({ }, define: { global: {}, + 'process.env': {}, }, build: { rollupOptions: { diff --git a/src/Interfaces/Atlantis/Api.fs b/src/Interfaces/Atlantis/Api.fs index 15393a37..b1291e66 100644 --- a/src/Interfaces/Atlantis/Api.fs +++ b/src/Interfaces/Atlantis/Api.fs @@ -60,4 +60,10 @@ module Api = { markRead: Guid -> Async getMessages: unit -> Async - } \ No newline at end of file + } + + type Plume = { + startPlume: Hipster.Actors.PlumeJob -> Async + getSimInfo: int -> Async + getActiveJobs: Guid -> Async + } diff --git a/src/Interfaces/Hipster/Actors.fs b/src/Interfaces/Hipster/Actors.fs index 17c4d642..fd16202f 100644 --- a/src/Interfaces/Hipster/Actors.fs +++ b/src/Interfaces/Hipster/Actors.fs @@ -51,12 +51,19 @@ type PostdriftJob = { type PlumeJob = { name: string fvcom: Guid - pos: float * float + pos: LatLong + // TODO: Functions, could be constants + // saltfunc: unit + // tempfunc: unit + /// Example 4 and 0 temp: float salt: float + /// DOB Depth on Bottom depth: float + /// Pipe Angle theta: float transport: float + /// Pipe radius radius: float start: DateTime stop: DateTime @@ -65,7 +72,7 @@ type PlumeJob = { static member empty = { name = "" fvcom = System.Guid.Empty - pos = 0.0, 0.0 + pos = {Lat = 0.0; Long = 0.0} temp = 0.0 salt = 0.0 depth = 0.0 @@ -97,4 +104,4 @@ type IDriftersActor = type IPlumeActor = inherit IActor - abstract Submit: job: PlumeJob -> Task> \ No newline at end of file + abstract Submit: job: PlumeJob -> Task> diff --git a/src/Interfaces/Hipster/Job.fs b/src/Interfaces/Hipster/Job.fs index d8b538d5..ef18a9fc 100644 --- a/src/Interfaces/Hipster/Job.fs +++ b/src/Interfaces/Hipster/Job.fs @@ -12,38 +12,36 @@ type SlurmJobType = | DriftersJob | PlumeJob | UnknownJob - with - override x.ToString() = - match x with - | DriftersJob -> "Drifters" - | PlumeJob -> "Plume" - | UnknownJob -> "Unknown" - static member FromString (s: string) = - match s.ToLower() with - | "drifters" -> DriftersJob - | "plume" -> PlumeJob - | _ -> UnknownJob + override x.ToString() = + match x with + | DriftersJob -> "Drifters" + | PlumeJob -> "Plume" + | UnknownJob -> "Unknown" + static member FromString(s: string) = + match s.ToLower () with + | "drifters" -> DriftersJob + | "plume" -> PlumeJob + | _ -> UnknownJob type SlurmMessageType = | Status | Progress | Message - with - override x.ToString() = - match x with - | Status -> "status" - | Progress -> "progress" - | Message -> "message" - static member FromString (s: string) = - match s.ToLower() with - | "status" -> Status - | "progress" -> Progress - | _ -> Message + override x.ToString() = + match x with + | Status -> "status" + | Progress -> "progress" + | Message -> "message" + static member FromString(s: string) = + match s.ToLower () with + | "status" -> Status + | "progress" -> Progress + | _ -> Message type SlurmJobStatusMsg = { jobId: int agentId: string - jobType: string // deserialized by asp.net + jobType: string // deserialized by asp.net messageType: string // deserialized by asp.net content: string } @@ -93,3 +91,6 @@ type DriftersPolicy = match x with | SubmitSedimentation v -> v | SubmitTransport v -> v + +type LatLong = { Lat: float; Long: float } +