Application code structure
Describe code architecture.
This application is organized in two parts:
A minimalist Node.js static server, located in the
server/
directory.A complete Elm client application, located in the
client/
directory.
Server side: Express (Node.js)
We can simply show the whole code of the server since it fits in 21 lines of code, including imports:
It's basically serving the content of the dist/
directory statically, with gzip compression since the generated Main.js
file by the client application is roughly 1.2 MB raw but 150 KB gzipped.
Client side: Elm
The whole client application, running in your browser is an Elm application. Elm isn't a JavaScript framework, it's functional programming language compiling to JavaScript to run in browsers. Its syntax is inherited from Haskell but far simpler. I'd even argue that it is probably the best language to start programming in, thanks to its super helpful compiler.
The application architecture
Since this is an Elm application, it follows TEA (The Elm Architecture). The simplest way of describing TEA is as a unidirectional data transformation flow. The central entity is the Model
. It contains all and every information about our application state. The visual aspect of our application is called the View
. The View
is generated from and only from the Model
. Meaning there is a function called view
which takes a Model
, and returns a View
(basically an HTML document). And finally, all events generate messages, of type Msg
. We have a function called update
, changing the model by reacting to those messages, closing the loop.
The above diagram is a simplistic representation of TEA from Elm point of view. In Elm code, all functions are "pure" in the functional jargon. Meaning that there is no side effect, meaning that outputs of functions are entirely defined by inputs. There cannot be global variables, real world events, network interaction etc. Basically a program like that would be running in a predestined way from its start to its end. We do not want that, since we want to load images, interact with them etc. This is why an elm program is attached to the Elm runtime, a huge piece of work, provided by the language, transforming all real world events ("side effects") into our defined set of messages, of type Msg
.
Our actual program is better described by the following diagram. The main challenge to keep functions "pure" is to find a way to describe side effects without performing them. In a typical Elm program, side effects are described in three places:
View attributes as DOM events listeners.
Commands (
Cmd
) generated by the update function, like making http requests.Subscriptions (
Sub
) to outside world events like the time ticking.
The Elm runtime takes those side effect descriptions, perform them, and, whenever there is a result / an answer, transforms it into a Msg
of our choosing and route it to the update function.
The model states
The model, that we described previously as the entity containing the complete state of our application, is a record containing four fields:
The
viewParameters
field contains cached parameters precomputed for the view function, like the current device type (phone, tablet, computer) depending on window dimension, or other view-related parameters.The
state
field contains the main state of our application i.e. the images loaded, the configuration chosen, the annotations performed etc. We will describe this further.The
viewer
field contains the entity recording movements coordinates relative to the image frame and zoom ratio, in order to display correctly the annotations on the image.Finally,
dragState
is constantly tracking the state of the pointer. Whether it is currently up or down and dragging from a given initial position.
The state
is of type State
defined as follows:
It can be modeled as a finite state machine, visualized as the following diagram:
The application available at https://annotation-app.pizenberg.fr/ starts in state 0 (NothingProvided
) and provides a button to load images. In doing so, you reach state 1a (ImagesProvided
), and then by providing a configuration you finally reach the state 2 (AllProvided
). It is the state in which we can annotate images. State 1b (ConfigProvided
) should only be reachable by providing a config in startup flags (see last section) and no image. Two messages called LoadImages
and ConfigLoaded
produce transitions in the state machine. They are detailed soon.
You may notice that images and tools are regrouped in collections called Zipper
. A Zipper
(wikipedia, lyahfgg) is a very usefull collection. In our case, it is more or less a non empty list, in which at all times, there is one element selected. Since there is always a tool selected (movement or annotation tool) and always an image selected, it is the perfect collection type for this job.
The messages
All modifications of the model are understood by looking at the Msg
type. The update
function then performs the modifications of the model described by such a message.
The following type definition may have change since this documentation was written. But for the purpose of describing the application behavior, it shouldn't be an issue.
The
WindowResizes
message is triggered when the application is resized. In the update function, it takes the new size and recomputes some view parameters and the annotation viewer.A
PointerMsg
message is triggered by pointer events (mouse, touch, etc.). In the update function, this is the message activating all the annotations logic code of our application.The messages
SelectImage
,SelectTool
andSelectClass
with their corresponding integer ids are generated when clicking on images, tools and classes in their respective bars.Files are handled by five messages. Those messages correspond to actions not entirely doable in Elm. They are thus partially handled through ports in JavaScript. Ports are explained in the Elm guide if you want to know more about them.
When selecting images in the file explorer to load, a
LoadImages
message is generated with a list of the images files and their names as identifiers. The update function will ask JavaScript to load each image into animg
tag since this is not possible within Elm. For every image correctly loaded into animg
tag, JavaScript sends it back to Elm in theImageLoaded
message, providing a local url, corresponding to the image in memory.The same round trip to JavaScript is done to load the text content of the configuration Json file, with the messages
LoadConfig
andConfigLoaded
.The
Export
message causes the application to serialize into Json all the annotations, and asks the user to save the generated file. It is triggered by clicking on the export button of the top action bar.
Whenever an effect should change the zooming level of the drawing area, a
ZoomMsg
message should be generated. The update function will update relevant model fields according to the message.Finally, the
RemoveLatestAnnotation
message is stating that we should remove latest annotation of the current tool selected. The update function takes care of doing it, if possible.
The view
The view of this application is based on four components, each implemented in its own module:
The top action bar (
src/View/ActionBar.elm
)The center annotations viewer area (
src/View/AnnotationsArea.elm
)The right images sidebar (
src/View/DatasetSideBar.elm
)The left classes sidebar (
src/View/ClassesSideBar.elm
)
Each of those view components may have different versions depending on the current state of the application. The action bar for example have four versions:
emptyView : Parameters msg -> Element ...
: provides a bar with only one button to load images, in order to transition to theImagesProvided
state.viewImages : Parameters msg -> Element ...
: provides an additional button to load a config, in order to transition to theAllProvided
state.viewAll : Parameters msg -> Zipper Tool -> Element ...
: provides a fully functional action bar, with the tools as specified in the configuration Json file.viewConfig : Parameters msg -> Zipper Tool -> Element ...
: provides a preview of the configuration loaded but not yet actionable. This is not visible in the "normal" app transitions, only if start flags (see next section) provided in theindex.html
file provide a config but no image.
Startup and interactions with JavaScript
As explained at the end of the crowd-sourcing page, the application is started in the index.html
file with initial values called flags. Currently, flags contain four objects.
deviceSize
provides the initial size of the area allocated to the application in the html page.mturkMode
toggles a mode in which the action bar view does not show the buttons to load images or load a config. It also replaces the icon button to export annotation by a textual "Submit" button.images
contains a list of images (url, width, height) with an already available url, without having to locally load them in memory from the disk.config
can provide an already defined configuration, to avoid having to load a Json file later.
The "normal" application, introduced in the Getting started page is started with no preloaded image or configuration. That is why it starts in the NothingProvided
state. In order to reach the ConfigProvided
state (1b), one should use flags with a valid config, and an empty images array.
Once the application is started, almost everything is managed using the Elm language. There are some interactions however, that still require communication with JavaScript due to lack of cover of some Web APIs in the Elm language. All those interactions with JavaScript are called ports, and are specified in two files:
The file
src/Ports.elm
in Elm side.The file
static/ports.js
in JavaScript side.
Those files are extremely small, and as such, we can put them here for explanations.
Each name in the previous Ports.elm
file is explicit so I will just comment on the return types of those ports. Each port returns either a Cmd
(command) or a Sub
(Subscription). From Elm side, a port generating a command is an outgoing port (to JavaScript) and a port generating a subscription is an incoming port. From JavaScript side, the relation to ports are reversed. Consequently, in the JS code, we "subscribe" to Elm outgoing ports and "send" to an Elm incoming port. You can see that in the ports.js
file. Ports usually come in outgoing-incoming pairs but not necessarily. The resizes
incomming port has no outgoing counterpart, and the reverse is true for the export
port.
Currently these ports are only designed to fit the needs of our Elm application. But if we shift a bit our point of view, those ports can actually be interpreted as an API. Since the Elm language is not very popular (yet ^^), one could simply create ports exposing to JavaScript functionalities (messages) of the application, and treat the Elm application just like a remote server.
Last updated