iD Architecture: Modes, Behavior, and Operations A tour of the code that implements the user interface of iD
The second in a three-part series on the technical architecture of the iD editor. Yesterday we looked at iD’s core data types and actions.
Closer to the user interface of iD lie three abstractions for a user’s interaction with the system: modes, behaviors, and operations. Modes contain the code behind the ‘Browse’, ‘Point’, ‘Line’ and ‘Area’ buttons at the top left, behaviors provide a means of reusing common bits of code across multiple modes, and operations provide the user interface for actions. Let’s look at each in detail.
Modes are settings that change the behavior of an application: for instance, drawing applications tends to have a ‘move’ mode, which moves the picture, as well as a ‘brush’ mode, in which the same clicks on the picture draw lines.
Modes are useful because they allow user interfaces to be customized for certain narrow uses, and for common actions, like clicks and drags, to have multiple meanings. However, they pose a challenge for new users, who might not discover them and have access to functionality, or will not realize what mode they’re currently in.
In iD modes are manifested in the interface by the four buttons at the top left:
The modality of existing OSM editors runs the gamut from Potlatch 2, which is nearly modeless, to JOSM, which sports half a dozen modes out of the box and has many more provided by plugins. iD seeks a middle ground: too few modes can leave new users unsure where to start, whereas too many can be overwhelming.
iD’s initial mode is ‘Browse’: one can drag the map and select entities to edit. From there, users can enter three geometrically-oriented drawing modes, Point, Line, and Area, through the mode buttons or key-shortcuts.
The geometric modes are also split into a mode for the initial point drawn object and another for continuing an existing object. The exception to this rule is points, which have a single step.
Selection is handled by a specific ‘Select’ mode, which displays editing tools for tags and operations.
The API for each mode consists of two methods:
enter method sets up all the behavior that should be
present when that mode is active. This typically means adding listeners to
DOM events that will be triggered on map elements, installing keybindings, and
showing certain parts of the interface like the inspector in
exit method does the opposite, removing the behavior installed by the
enter method. Together the two methods ensure that modes are self-contained
and exclusive: each mode knows exactly the behavior that is specific to that
mode, and exactly one mode’s behavior is active at any time.
Modes share functionality, which we abstract into behaviors. For
example, in both the Browse and Draw modes, iD indicates interactive map elements by drawing a halo around them on mouse hover.
Instead of duplicating the code to implement this behavior in all these modes, we
extract it to
Behaviors are inspired by d3’s
behaviors. Like d3’s
d3.behavior.drag, each iD behavior is a function that takes as input a d3 selection
(assumed to consist of a single element) and installs DOM event bindings
necessary to implement the behavior.
iD.behavior.Hover, for example,
installs bindings for the
mouseout events that add and
hover class from map elements.
Because certain behaviors are appropriate to some but not all modes, we need
the ability to remove a behavior when entering a mode where it is not
appropriate. d3’s own behaviors
don’t offer this functionality yet.
Each behavior implements an
off function that uninstalls the behavior.
This is very similar to the
exit method of a mode, and in fact many modes do
little else but uninstall behaviors in their
To make modes and behaviors more concrete, here’s an annotated extract from
Operations wrap actions, providing their user-interface: tooltips, key
bindings, and the logic that determines whether an action can be
performed given the current map state and selection. Each operation is
constructed with the list of IDs which are currently selected and a
object which provides access to the history and other important parts of iD’s
internal state. After being constructed, an operation can be queried as to
whether or not it should be made available (i.e., show up in the context menu)
and if so, if it should be enabled.
We make a distinction between availability and enabled state for the sake of learnability: most operations are available so long as an entity of the appropriate type is selected. Even if it remains disabled for other reasons (e.g., because you can’t split a way on its start or end vertex), a new user can still learn that “this is something I can do to this type of thing”, and a tooltip can provide an explanation of what that operation does and the conditions under which it is enabled.
To execute an operation, call it as a function, with no arguments. The typical
operation will perform the appropriate action, creating a new undo state in
the history, and then enter the appropriate mode. For example,
iD.actions.Split, then enters
iD.modes.Select with the resulting ways selected.
We wrap up our architecture series with a discussion of map rendering and other UI components.
Devlogging work on the OpenStreetMap project by the MapBox team.
Much of this work is currently focused on improvements to OpenStreetMap funded by the Knight Foundation