Update ARCHITECTURE.md

Could use a bit more, but at least it is up to date
(closes #3234)
This commit is contained in:
Bryan Housel
2016-11-15 00:27:17 -05:00
parent 9b7d79a3fe
commit f8e022c7b4
+108 -91
View File
@@ -1,59 +1,74 @@
## d3
## iD Architecture
iD is written in a modular code style using ES6 modules. The modules are bundled
with [rollup.js](http://rollupjs.org/). iD eventually aims to be a reusable,
modular library to kickstart other JavaScript-based tools for OpenStreetMap.
### 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
[d3.request](https://github.com/d3/d3/blob/master/API.md#requests-d3-request), 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),
[d3.dispatch](https://github.com/d3/d3/blob/master/API.md#dispatches-d3-dispatch),
which provides a callback-based [Observer
pattern](http://en.wikipedia.org/wiki/Observer_pattern) between different
parts of iD;
[d3.geoPath](https://github.com/mbostock/d3/wiki/Geo-Paths#wiki-path), which
[d3.geoPath](https://github.com/d3/d3/blob/master/API.md#paths), which
generates SVG paths for lines and areas; and
[d3.behavior.zoom](https://github.com/mbostock/d3/wiki/Zoom-Behavior#wiki-zoom),
[d3.zoom](https://github.com/d3/d3/blob/master/API.md#zooming-d3-zoom),
which implements map panning and zooming.
## Core
The iD *core* implements OSM data types, a graph of OSM objects'
relationships to one another, an undo/redo history for changes made during
editing, and a couple of important auxiliary classes. It eventually aims
to be a reusable, modular library to kickstart other JavaScript-based
tools for OpenStreetMap.
### Core Module
The OSM data model includes three basic data types: nodes, ways, and
relations.
The iD *core* module implements the basic datastructures needed to support
browser-based editing:
* A _node_ is a point type, having a single geographic coordinate.
* A _way_ is an ordered list of nodes.
* A _relation_ groups together nodes, ways, and other relations to provide
* `iD.coreContext` - container for all iD "global" objects and bootstrap code
* `iD.coreGraph` - graph of objects and their relationships to one another
* `iD.coreHistory` - undo/redo history for changes made during editing
* `iD.coreDifference` - efficiently determine the difference between two graphs
* `iD.coreTree` - performs fast spatial indexing of the loaded objects
### OSM Module
The iD *osm* module includes classes which represent the basic OpenStreetMap
objects: nodes, ways, and relations.
* `iD.osmNode` - a _node_ is a point type, having a single geographic coordinate
* `iD.osmWay` - a _way_ is an ordered list of nodes
* `iD.osmRelation` - 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
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`.
* `iD.osmEntity` - common base class for `iD.osmNode`, `iD.osmWay`, `iD.osmRelation`
These three classes inherit from a common base, `iD.osmEntity`.
This is the only use of classical inheritance in iD, but it's justified
by the common functionality of the types. Generically, we refer to a
by the common functionality of the types. Generically, we refer to a
node, way or relation as an _entity_.
Every entity has an _ID_ either assigned by the OSM database or
a negative, local identifier assigned by iD for newly-created objects.
IDs from the OSM database as treated as opaque strings; no
Every entity has a unique numeric `id`. By convention, positive numbers are
assigned by the OSM database for saved features, and negative numbers are
assigned by the iD editor for local newly-created objects.
`id` values 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). The three
types of entities have separate ID spaces: a
node can have the same numeric ID as a way or a relation. Instead of segregating
ways, nodes, and other entities into different datastructures,
iD internally uses fully-unique IDs generated 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.
begin with a minus sign (and thus will not conflict with local `id` values).
The three types of entities have separate `id` spaces: a node can have the
same numeric `id` as a way or a relation. Instead of segregating ways, nodes,
and other entities into different datastructures, iD internally uses fully-unique
`id` values generated 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,
@@ -66,19 +81,30 @@ structure](http://en.wikipedia.org/wiki/Persistent_data_structure).
Since iD is an editor, it must allow for new versions of entities. 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
entity level, this takes the form of methods such as `iD.osmNode#move`, which
returns a new node object that has the same `id` and `tags` as the original, but a
different `loc` coordinate. More generically, `iD.osmEntity#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.
![](http://farm9.staticflickr.com/8087/8508309757_ccf5b6f09b_o.png)
Entities are related to one another: ways have many nodes and relations have
many members. To render a map of a certain area, iD needs a
many members.
The osm module also includes code related to special kinds of objects in OpenStreetMap.
* `iD.osmIntersection` - code for working with turn restrictions
* `iD.osmLanes` - code for working with traffic and turn lanes
* `iD.osmMultipolygon` - code for working with multipolygon relations
### Editing OSM
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
relationships. `iD.coreGraph` provides this functionality. The core of a graph is
a map between `id`s 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 minimize
@@ -87,7 +113,7 @@ copying the entire graph.
This persistent data structure approach is similar to the internals of
the [git](http://git-scm.com/) revision control system.
The final major component of the core is `iD.History`, which tracks the changes
The final major component of the core is `iD.coreHistory`, which tracks the changes
made in an editing session and provides 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
@@ -102,41 +128,41 @@ Instead of changing a single copy of local data and having to implement
an 'undo' for each specific action, actions in iD do not need to be aware
of history and the undo system.
Finally, we have the auxiliary classes `iD.Difference` and `iD.Tree`.
Finally, we have the auxiliary classes `iD.coreDifference` and `iD.coreTree`.
`iD.Difference` encapsulates the difference between two graphs, and knows how to calculate the
`iD.coreDifference` encapsulates the difference between two graphs, and knows how to calculate the
set of entities that were created, modified, or deleted, and need to be redrawn.
```js
var a = iD.Graph(), b = iD.Graph();
var a = iD.coreGraph(), b = iD.coreGraph();
// (fill a & b with data)
var difference = iD.Difference(a, b);
var difference = iD.coreDifference(a, b);
// returns entities created between and b
// returns entities created between a and b
difference.created();
```
`iD.Tree` calculates the set of downloaded entities that are visible in the
`iD.coreTree` calculates the set of downloaded entities that are visible in the
current map view. To calculate this quickly during map
interaction, it uses an [R-tree](http://en.wikipedia.org/wiki/R-tree).
```js
var graph = iD.Graph();
var graph = iD.coreGraph();
// (load OSM data into graph)
// this tree indexes the contents of the graph
var tree = iD.Tree(graph);
var tree = iD.coreTree(graph);
// quickly pull all features that intersect with an extent
var features = tree.intersects(
iD.geoExtent([0, 0], [2, 2]), tree.graph());
```
## Actions
### Actions Module
In iD, an _action_ is a function that accepts a graph as input and returns a
new, modified graph as output. Actions typically need other inputs as well; for
example, `iD.actionDeleteNode` also requires the ID of a node to delete. The
example, `iD.actionDeleteNode` also requires the `id` of a node to delete. The
additional input is passed to the action's constructor:
```js
@@ -165,10 +191,10 @@ knowledge of the OpenStreetMap data model. It is our hope that JavaScript
based tools for OpenStreetMap can reuse the iD's core implementation,
significantly reducing the work necessary to create a robust tool.
## Modes
### Modes Module
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
parts of the architecture that implement the user interface for iD. Modes are
manifested in the interface by the three buttons at the top left:
![Mode buttons](docs/img/modes.png)
@@ -199,7 +225,7 @@ The `exit` mode does the opposite, removing the behavior installed by the
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
### Behavior Module
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
@@ -208,7 +234,7 @@ of duplicating the code to implement this behavior in all these modes, we
extract it to `iD.behaviorHover`.
_Behaviors_ take their inspiration from [d3's
behaviors](https://github.com/mbostock/d3/wiki/Behaviors). Like d3's `zoom`
behaviors](https://github.com/d3/d3/blob/master/API.md). 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,
@@ -223,12 +249,12 @@ 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 Module
_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`
constructed with the list of `id`s 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)
@@ -250,11 +276,11 @@ the history, and then enter the appropriate mode. For example,
`iD.operationSplit` performs `iD.actionSplit`, then enters
`iD.modeSelect` with the resulting ways selected.
## Map Rendering
### Renderer and SVG Modules
Finally, we get to the parts of iD that actually draw and manipulate the map
entities on screen. The rendering is coordinated by `iD.Map`, which takes care
of setting up a [Spherical Mercator](http://bl.ocks.org/mbostock/3757132)
entities on screen. The rendering is coordinated by `iD.rendererMap`, which
takes care of setting up a [Spherical Mercator](http://bl.ocks.org/mbostock/3757132)
projection and the [zoom
behavior](https://github.com/mbostock/d3/wiki/Zoom-Behavior), and provides
accessors for such things as the current zoom level and map center.
@@ -271,7 +297,7 @@ entity types of the OSM data model:
For each of these geometric types, `iD.svg` has a corresponding module:
`iD.svgPoints`, `iD.svgVertices`, `iD.svgLines`, and `iD.svgAreas`. To
render entities on screen, `iD.Map` delegates to these modules. Internally,
render entities on screen, `iD.rendererMap` delegates to these modules. Internally,
they make heavy use of [d3 joins](http://bost.ocks.org/mike/join/) to
manipulate the SVG elements that visually represent the map entities. When an
entity is rendered for the first time, it is part of the _enter_ selection,
@@ -289,53 +315,44 @@ via CSS at either the key or key-value levels. SVG elements also receive a
class corresponding to their entity type (`node`, `way`, or `relation`) and
one corresponding to their geometry type (`point`, `line`, or `area`).
The `iD.svg` namespace has a few other modules that don't have a one-to-one
The `iD.svg` module has a few other submodules that don't have a one-to-one
correspondence with entities:
* `iD.svgMidpoints` renders the small "virtual node" at the midpoint between
* `iD.svgMidpoints` - draws the small "virtual node" at the midpoint between
two vertices.
* `iD.svgLabels` renders the textual
[labels](http://mapbox.com/osmdev/2013/02/12/labeling-id/).
* `iD.svgLayers` sets up a number of layers that ensure that map elements
* `iD.svgLabels` - draws textual labels
* `iD.svgLayers` - sets up a number of layers that ensure that map elements
appear in an appropriate z-order.
* `iD.svgOsm` - sets up the OSM-specific data layers
* `iD.svgGpx` - draws gpx traces
* `iD.svgDebug` - draws debugging information
## Other UI
### Other UI
iD provides a lot of user interface elements other than the core map component:
the page footer, the interface for saving changes, the splash screen you see
the first time you use iD, the geocoding and background layer controls, and the
tag/preset editor, for example.
the first time you use iD, the map controls, and the tag/preset editor, for example.
![Geocoder UI](docs/img/geocoder.png)
The implementations for all non-map UI components live in the `iD.ui` namespace.
Many of the modules in this namespace follow a pattern for reusable d3
The implementations for all non-map UI components live in the `iD.ui` module.
Many of the submodules under the `ui` module follow a pattern for reusable d3
components [originally suggested](http://bost.ocks.org/mike/chart/) by Mike
Bostock in the context of charts. The entry point to a UI element is a
constructor function, e.g. `iD.uiGeocoder()`. The constructor function may
Bostock in the context of charts. The entry point to a UI element is a
constructor function, e.g. `iD.uiViewOnOSM()`. The constructor function may
require a set of mandatory arguments; for most UI components exactly one
argument is required, a `context` object produced by the top-level `iD()`
function.
argument is required, a `context`.
A component needs some way to be rendered on screen by creating new DOM
elements or manipulating existing elements. This is done by calling the
component as a function, and passing a d3 selection where the component should
render itself:
The constructor function returns a draw function which accepts a d3 selection.
Drawing is then accomplished with
[d3.selection#call](https://github.com/d3/d3-selection/blob/master/README.md#selection_call):
```
var container = d3.select('body').append('div')
.attr('class', 'map-control geocode-control');
```js
footer = footer.enter()
.append('div')
.attr('class', 'footer')
.merge(footer);
var geocoder = iD.uiGeocoder(context)(container);
```
Alternatively, and more commonly, the same result is accomplished with
[d3.selection#call](https://github.com/mbostock/d3/wiki/Selections#wiki-call):
```
d3.select('body').append('div')
.attr('class', 'map-control geocode-control')
.call(iD.uiGeocoder(context));
footer
.call(uiViewOnOSM(context).entityID(entityID));
```
Some components are reconfigurable, and some provide functionality beyond