mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
Start on architecture docs
This commit is contained in:
203
ARCHITECTURE.md
Normal file
203
ARCHITECTURE.md
Normal file
@@ -0,0 +1,203 @@
|
||||
## d3
|
||||
|
||||
[d3](http://d3js.org/) is the primary library used by iD. It is used for
|
||||
rendering the map data as well as many sorts of general DOM manipulation tasks
|
||||
for which jQuery would often be used.
|
||||
|
||||
Notable features of d3 that are used by iD include
|
||||
[d3.xhr](https://github.com/mbostock/d3/wiki/Requests#wiki-d3_xhr), which is
|
||||
used to make the API requests to download data from openstreetmap.org and save
|
||||
changes;
|
||||
[d3.dispatch](https://github.com/mbostock/d3/wiki/Internals#wiki-d3_dispatch),
|
||||
which provides a callback-based [Observer
|
||||
pattern](http://en.wikipedia.org/wiki/Observer_pattern) between different
|
||||
parts of iD;
|
||||
[d3.geo.path](https://github.com/mbostock/d3/wiki/Geo-Paths#wiki-path), which
|
||||
generates SVG paths for lines and areas; and
|
||||
[d3.behavior.zoom](https://github.com/mbostock/d3/wiki/Zoom-Behavior#wiki-zoom),
|
||||
which implements map panning and zooming.
|
||||
|
||||
## Core
|
||||
|
||||
The iD *core* implements the OSM data types, a graph of OSM object's
|
||||
relationships to each other, and an undo/redo history for changes made during
|
||||
editing. It aims to be generic enough to be used by other JavaScript-based
|
||||
tools for OpenStreetMap.
|
||||
|
||||
Briefly, the OSM data model includes three basic data types: nodes, ways, and
|
||||
relations. A _node_ is a point type, having a single geographic coordinate. A
|
||||
_way_ is an ordered list of nodes. And a _relation_ groups together nodes,
|
||||
ways, and other relations to provide free-form higher-level structures. Each
|
||||
of these three types has _tags_: an associative array of key-value pairs which
|
||||
describe the object.
|
||||
|
||||
In iD, these three types are implemented by `iD.Node`, `iD.Way` and
|
||||
`iD.Relation`. These three classes inherit from a common base, `iD.Entity`
|
||||
(the only use of classical inheritance in iD). Generically, we refer to a
|
||||
node, way or relation as an _entity_.
|
||||
|
||||
Every entity has an _ID_ either assigned by the OSM database, or, for an
|
||||
entity that is newly created, constructed as a proxy consisting of a negative
|
||||
numeral. IDs from the OSM database as treated as opaque strings; no
|
||||
[assumptions](http://lists.openstreetmap.org/pipermail/dev/2013-February/026495.html)
|
||||
are made of them other than that they can be compared for identity and do not
|
||||
begin with a minus sign (and thus will not conflict with proxy IDs). In fact,
|
||||
in the OSM database the three types of entities have separate ID spaces; a
|
||||
node can have the same ID as a way, for instance. Because it is useful to
|
||||
store heterogeneous entities in the same datastructure, iD ensures that every
|
||||
entity has a fully-unique ID by prefixing each OSM ID with the first letter of
|
||||
the entity type. For example, a way with OSM ID 123456 is represented as
|
||||
'w123456' within iD.
|
||||
|
||||
iD entities are *immutable*: once constructed, an `Entity` object cannot
|
||||
change. Tags cannot be updated; nodes cannot be added or removed from ways,
|
||||
and so on. Immutability makes it easier to reason about the behavior of an
|
||||
entity: if your code has a reference to one, it is safe to store it and use it
|
||||
later, knowing that it cannot have been changed outside of your control. It
|
||||
also makes it possible to implement the entity graph (described below) as an
|
||||
efficient [persistent data
|
||||
structure](http://en.wikipedia.org/wiki/Persistent_data_structure). But
|
||||
obviously, iD is an editor, and must allow entities to change somehow. The
|
||||
solution is that all edits produce new copies of anything that changes. At the
|
||||
entity level, this takes the form of methods such as `iD.Node#move`, which
|
||||
returns a new node object that has the same ID and tags as the original, but a
|
||||
different coordinate. More generically, `iD.Entity#update` returns a new
|
||||
entity of the same type and ID as the original but with specified properties
|
||||
such as `nodes`, `tags`, or `members` replaced.
|
||||
|
||||
Entities are related to one another: ways have many nodes and relations have
|
||||
many members. In order to render a map of a certain area, iD needs a
|
||||
datastructure to hold all the entities in that area and traverse these
|
||||
relationships. `iD.Graph` provides this functionality. The core of a graph is
|
||||
a map between IDs and the associated entities; given an ID, the graph can give
|
||||
you the entity. Like entities, a graph is immutable: adding, replacing, or
|
||||
removing an entity produces a new graph, and the original is unchanged.
|
||||
Because entities are immutable, the original and new graphs can share
|
||||
references to entities that have not changed, keeping memory use to a minimum.
|
||||
If you are familiar with how git works internally, this persistent data
|
||||
structure approach is very similar.
|
||||
|
||||
The final component of the core is comprised of `iD.History` and
|
||||
`iD.Difference`, which track the changes made in an editing session and
|
||||
provide undo/redo capabilities. Here, the immutable nature of the core types
|
||||
really pays off: the history is a simple stack of graphs, each representing
|
||||
the state of the data at a particular point in editing. The graph at the top
|
||||
of the stack is the current state, off which all rendering is based. To undo
|
||||
the last change, this graph is popped off the stack, and the map is
|
||||
re-rendered based on the new top of the stack. Contrast this to a mutable
|
||||
graph as used in JOSM and Potlatch: every command that changes the graph must
|
||||
implement an equal and opposite undo command that restores the graph to the
|
||||
previous state.
|
||||
|
||||
## Actions
|
||||
|
||||
In iD, an _action_ is a function that accepts a graph as input and returns a
|
||||
modified graph as output. Actions typically need other inputs as well; for
|
||||
example, `iD.actions.DeleteNode` also requires the ID of a node to delete. The
|
||||
additional input is passed to the action's constructor:
|
||||
|
||||
``` var action = iD.actions.DeleteNode('n123456'); // construct the action var
|
||||
newGraph = action(oldGraph); // apply the action ```
|
||||
|
||||
iD provides actions for all the typical things an editor needs to do: add a
|
||||
new entity, split a way in two, connect the vertices of two ways together, and
|
||||
so on. In addition to performing the basic work needed to accomplish these
|
||||
things, an action typically contains a significant amount of logic for keeping
|
||||
the relationships between entities logical and consistent. For example, an
|
||||
action as apparently simple as `DeleteNode`, in addition to removing the node
|
||||
from the graph, needs to do two other things: remove the node from any ways in
|
||||
which it is a member (which in turn requires deleting parent ways that are
|
||||
left with just a single node), and removing it from any relations of which it
|
||||
is a member.
|
||||
|
||||
As you can imagine, implementing all these details requires an expert
|
||||
knowledge of the OpenStreetMap data model. It is our hope that JavaScript
|
||||
based tools for OpenStreetMap can reuse the implementations provided by iD in
|
||||
other contexts, significantly reducing the work necessary to create a robust
|
||||
tool.
|
||||
|
||||
## Modes
|
||||
|
||||
With _modes_, we shift gears from abstract data types and algorithms to the
|
||||
parts of the architecture that implement the user interface for 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
|
||||
almost entirely 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, while too many can be
|
||||
overwhelming.
|
||||
|
||||
iD's user-facing modes consist of a base "Browse" mode, in which you can move
|
||||
around the map and select and edit entities, and three geometrically-oriented
|
||||
drawing modes: Point, Line, and Area. In the code, these are broken down a
|
||||
little bit more. There are separate modes for when an entity is selected
|
||||
(`iD.modes.Select`) versus when nothing is selected (`iD.modes.Browse`), and
|
||||
each of the geometric modes is split into one mode for starting to draw an
|
||||
object and one mode for continuing an existing object (with the exception of
|
||||
`iD.modes.AddPoint`, which is a single-step operation for obvious reasons).
|
||||
|
||||
The code interface for each mode consists of a pair of methods: `enter` and
|
||||
`exit`. In the `enter` method, a mode sets up all the behavior that should be
|
||||
present when that mode is active. This typically means binding callbacks to
|
||||
DOM events that will be triggered on map elements, installing keybindings, and
|
||||
showing certain parts of the interface like the inspector in `Select` mode.
|
||||
The `exit` mode 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.
|
||||
|
||||
## Behavior
|
||||
|
||||
Certain behaviors are common to more than one mode. For example, iD indicates
|
||||
interactive map elements by drawing a halo around them when you hover over
|
||||
them, and this behavior is common to both the browse and draw modes. Instead
|
||||
of duplicating the code to implement this behavior in all these modes, we
|
||||
extract it to `iD.behavior.Hover`.
|
||||
|
||||
_Behaviors_ take their inspiration from [d3's
|
||||
behaviors](https://github.com/mbostock/d3/wiki/Behaviors). Like d3's `zoom`
|
||||
and `drag`, each iD behavior is a function that takes as input a d3 selection
|
||||
(assumed to consist of a single element) and installs the DOM event bindings
|
||||
necessary to implement the behavior. The `Hover` behavior, for example,
|
||||
installs bindings for the `mouseover` and `mouseout` events that add and
|
||||
remove a `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. (This is functionality [not yet
|
||||
provided](https://github.com/mbostock/d3/issues/894) by d3's own behaviors.)
|
||||
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 `exit` methods.
|
||||
|
||||
## Operations
|
||||
|
||||
_Operations_ wrap actions, providing their user-interface: tooltips, key
|
||||
bindings, and the logic that determines whether an action can be validly
|
||||
performed given the current map state and selection. Each operation is
|
||||
constructed with the list of IDs which are currently selected and a `context`
|
||||
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.operations.Split` performs `iD.actions.Split`, then enters
|
||||
`iD.modes.Select` with the resulting ways selected.
|
||||
|
||||
## Rendering and other UI
|
||||
BIN
img/modes.png
Normal file
BIN
img/modes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
img/operations.png
Normal file
BIN
img/operations.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Reference in New Issue
Block a user