ODIN Web Client User Interface Library
odin-rs
comes with its own web client user interface library that provides the common UI wdgets (Window
, List
, CheckBox
etc.).
The basic reasons why we do not use available 3rd party libraries is laid out in design principles but
there are also application specific ones, namely:
(a) UI components have to be compact. Normally UI libraries are for rendering full web pages but in ODIN web applications the main information is the geospatial display (virtual globe) that forms the background of the page. UI elements should not distract from this and have to support moving them around. Our use case is much closer to a traditional desktop user interface (inside a browser page) than it is to normal web page design.
(b) UI widgets have to support structured, dynamic data. The main use of UI components in odin-rs
web applications is to display
layer specific alphanumeric data that can be dynamically updated through a websocket, and to control the CesiumJS / WebGL
rendering of selected data items. If widgets only work with basic, generic types such as strings and numbers it would require
a large amount of glue/adapter code in respective JS modules of such layers, which would be especially error prone in the context
of layer specific data items that are asynchronously updated at a high rate (e.g. for object tracking). This is particularly addressed
by our List
widget.
(c) Theme support. We not only want to support a wide range of display sizes/resolutions and OS platforms (possibly with native UI resemblance). Since we also target in-field applications there needs to be the end-user capability to choose between day/night displays and high/low contrast modes. This requires extensive configuration support both on the server and locally on clients.
The ui.js
module implements our library as a odin_server
asset. It uses a fairly straight-forward
DOM manipulation through Javascript in which each of its
components is represented by a DIV
element with odin-rs
specific
class names such as ui_window
or ui_list
. The UI layout could be specified in a plain HTML document (if it has a post-load script that calls initializeWindow(e)
) but this is not the usual case.
To keep (structural) layout, UI state and respective functions in one place we recommend using the layer specific JS modules for
all these aspects. This means UI components do not appear in the static HTML document source but are dynamically added when respective
JS modules get initialized.
The ui.js
module uses theui.css
CSS style sheet for basic layout of its widgets.
The - rarely changed - ui.css
stylesheet in turn uses a configured theme CSS (e.g. ui_theme_dark.css
) that solely consists of a
(user modifiable) extensive set of CSS custom properties for colors, font-families, font sizes and other theme related styles. The odin_cesium.js
UI does include a settings window that lets users choose between different
theme CSS files, and also supports modifying/storing/restoring respective CSS properties in browser local storage.
Widgets
odin_server/assets/ui.js
provides the following UI components that are implemented as `HTMLElements
Window
The Window
widget is one of the toplevel components provided by ui.js
. Each interactive odin-rs
service usually has one or more
layer/service specific windows that are defined in their JS module through a call of the Window(title,id,icon)(components..)
function like so:
import * as ui from "../odin_server/ui.js";
...
function createWindow() {
return ui.Window("GOES-R Satellites", "goesr", "./asset/odin_goesr/geo-sat-icon.svg")(
ui.Panel("data sets", true)(
ui.RowContainer()(
ui.CheckBox("lock step", toggleGoesrLockStep, "goesr.lockStep"),
...
ui.HorizontalSpacer(2),
ui.CheckBox("G16", toggleShowGoesrSatellite, "goesr.G16"),
...
),
ui.List("goesr.dataSets", 6, selectGoesrDataSet),
...
),
ui.Panel("hotspots", true)(
...
),
...
);
}
Note that the first argument group of the curried Window(..)(..)
function defines the titlebar and the second argument group
specifies the structured content and layout of a Window
instance.
Although this is not required Window
content components are usually organized into separate Panels
so that respective content can be
expanded/collapsed on demand in order to minimize UI screen space.
Windows
can be interactively moved through dragging their titlebar and shown/hidden through their associated icon (and titlebar buttons).
Normally, Window
instances are static, i.e. they are defined and instantiated in the module initialization code, but only shown after
user interaction (e.g. clicking on the icon). They also can be created/disposed dynamically, which happens for per-data-item views
(e.g. the ImageWindow
) or dialogs (e.g. to enter geospatial data such as polygons).
Creating, showing, hiding and disposing Windows
is normally done through standard behavior triggered by user interaction, i.e. JS modules
rarely use functions other than the Window(..)(..)
constructor mentioned above. There are no Window
specific callbacks that
can or have to be provided.
List
The List
widget and its derivate TreeList
are perhaps the most important components in the ui.js
library. They provide
scrollable, selectable lists of generic items. Those items are not restricted to simple string types - Lists
can be used
to display heterogenous collections of arbitrary objects. The goal is to avoid having to map application-specific data
(e.g. GoesrDataSet
) into UI-displayable data. This is achieved by supporting a column-oriented display configuration that
uses function parameters to specify what/how to display for each column.
The four groups of List
related functions are:
- construction (
List (id, maxRows, selectAction, clickAction, contextMenuAction, dblClickAction)
) - display configuration (
setListItemDisplayColumns (listElement, listAttrs, colSpecs)
) - data management (
setListItems (listElement, items)
,updateListItem (listElement, item)
,appendListItem (listElement, item)
,removeListItem (listElement, item)
,clearList (itemElement)
) - selection and selection callbacks (
setSelectedListItem(listElement, item)
, single- and double-click event handler functions)
The general pattern of using List
instances therefore is:
import * as ui from "../odin_server/ui.js";
...
var datasSetList = undefined;
var selectedDataSet = undefined;
createWindow();
initDataSetList();
...
function createWindow () {
return ui.Window(...)(
...
(dataSetList = ui.List("goesr.dataSets", 6, selectGoesrDataSet)), // ① List construction
...
);
}
function initDataSetList () { // ② display configuration
if (dataSetList) {
ui.setListItemDisplayColumns(dataSetList, ["fit", "header"], [
{ name: "sat", tip: "name of satellite", width: "3rem", attrs: [], map: e => e.sat.name },
{ name: "good", tip: "number of good pixels", width: "3rem", attrs: ["fixed", "alignRight"], map: e => e.nGood },
...
{ name: "date", tip: "last report", width: "8rem", attrs: ["fixed", "alignRight"], map: e => util.toLocalMDHMString(e.date) }
]);
}
}
... ui.setListItems( dataSetList, dataSets); ... // ③ data management
... ui.updateListItem( dataSetList, selectedDataSet); ...
... ui.setSelectedListItem( dataSetList, matchingItem); // ④ selection and selection callbacks
function selectGoesrDataSet (event) { // user selected item with single click
let item = event.detail.curSelection;
if (item) {
selectedDataSet = item;
...
}
}
The List
widget has a rich API that supports many more functions, reflecting that ODIN data is generally dynamic in nature and we
therefore need to support efficient update.
List
item columns do not have to map 1:1 into item properties. Columns can contain computed data and even widgets (e.g.
CheckBoxes
).
TreeList
TreeList
is a close cousin of List
that is used to display hierarchical data that can be interactively expanded/collapsed. In
fact, TreeList
is implemented as a List
that wraps application-specific items into generic nodes that retain the
column-oriented item display configuration for leaf- nodes. The mapping from application specific item collections into
generic nodes is based on the ExpandableTreeNode
class in the ui_data.js
module of odin_server
.
Since ExpandableTreeNode
is a display (UI) specific type this means data initialization of TreeLists
always involves
constructing a tree from application specific item collections like so:
import * as ui from "../odin_server/ui.js";
import * as uiData from "../odin_server/ui_data.js";
...
let tree = uiData.ExpandableTreeNode.from( items, e=>e.key);
ui.setTree( dirView, tree);
...
The second parameter of ExpandableTreeNode.from (items, pathExtractor, ..)
is a function (usually provided by a closure) that
defines how to compute the hierarchical pathname of an item that is the basis for constructing the tree.
Apart from the tree construction and the addition of item-wrapping nodes the TreeList
API has the same categories as List
:
construction, display configuration, data management and selection management. The typical pattern of TreeList
usage therefore
is:
import * as ui from "../odin_server/ui.js";
import * as uiData from "../odin_server/ui_data.js";
...
function createWindow(..){
... (dirList = ui.TreeList("share.dir.list", 15, "32rem", selectShareEntry)), ...
}
function initDirList() {
if (dirList) {
ui.setListItemDisplayColumns(dirList, ["fit", "header"], [
{ name: "show", tip: "render selected item", width: "2.5rem", attrs:[], map: e=>itemRenderCheckBox(e) },
...
{ name: "type", tip: "item type", width: "6rem", attrs: ["small"], map: e=> itemType(e) }
]);
}
}
function itemRenderCheckBox (e) {
return e && e.value ? ui.createCheckBox( isItemShowing(e), toggleShowItem) : "";
}
... let tree = ExpandableTreeNode.from( items, e=>e.key);
ui.setTree( dirView, tree); ...