diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..c0ff6b217 --- /dev/null +++ b/ARCHITECTURE.md @@ -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: + +![Mode buttons](img/modes.png) + +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. + +![Operations menu](img/operations.png) + +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..09dbab7ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing to iD + +Thinking of contributing to iD? High five! Here are some basics for our habits +so that you can write code that fits in perfectly. + +## Reporting Issues + +We'd love to hear what you think about iD, about any specific problems or +concerns you have. Here's a quick list of things to consider: + +Please [search for your issue before filing it: many bugs and improvements have already been reported](https://github.com/systemed/iD/issues/search?q=) + +To report a bug: + +* Write specifically what browser (type and version, like Firefox 22), OS, and browser extensions you have installed +* Write steps to replicate the error: when did it happen? What did you expect to happen? What happened instead? +* Please keep bug reports professional and straightforward: trust us, we share your dismay at software breaking. +* If you can, [enable web developer extensions](http://macwright.org/enable-web-developer-extensions/) and report the + Javascript error message. + +When in doubt, be over-descriptive of the bug and how you discovered it. + +To request a feature: + +* If the feature is available in some other software (like Potlatch), link to that software and the implementation. + We care about prior art. +* Understand that iD is meant to be a simple editor and doesn't aim to be + as complete or complicated as JOSM or similar. + +## Javascript + +We use the [Airbnb style for Javascript](https://github.com/airbnb/javascript) with +only one difference: + +**4 space soft tabs always for Javascript, not 2.** + +No aligned `=`, no aligned arguments, spaces are either indents or the 1 +space between expressions. No hard tabs, ever. + +Javascript code should pass through [JSHint](http://www.jshint.com/) with no +warnings. + +## HTML + +There isn't much HTML in iD, but what there is is similar to JS: 4 spaces +always, indented by the level of the tree: + +```html +
+
+
+``` + +## CSS + +Just like HTML and Javascript, 4 space soft tabs always. + +```css +.radial-menu-tooltip { + background: rgba(255, 255, 255, 0.8); +} +``` + +We write vanilla CSS with no preprocessing step. Since iD targets modern browsers, +feel free to use newer features wisely. + +## Tests + +Test your code and make sure it passes. Our testing harness requires [node.js](http://nodejs.org/) +and a few modules: + +1. [Install node.js](http://nodejs.org/) - 'Install' will download a package for your OS +2. Go to the directory where you have checked out `iD` +3. Run `npm install` +4. Run `npm test` to see whether your tests pass or fail. + +## Licensing + +iD is under the [WTFPL](http://www.wtfpl.net/). Some of the libraries it uses +are under different licenses. If you're contributing to iD, you're contributing +WTFPL code. + +## Submitting Changes + +Let's say that you've thought of a great improvement to iD - a change that +turns everything red (please do not do this, we like colors other than red). + +In your local copy, make a branch for this change: + + git checkout -b make-red + +Make your changes to source files. By source files we mean the files in `js/`. +the `iD.js` and `iD.min.js` files in this project are autogenerated - don't edit +them. + +So let's say you've changed `js/ui/confirm.js`. + +1. Run `jshint js/id` to make sure your code is clean +2. Run tests with `npm test` +3. Commit your changes with an informative commit message +4. [Submit a pull request](https://help.github.com/articles/using-pull-requests) to the `systemed/iD` project. diff --git a/Makefile b/Makefile index 70ac208d6..bb6844399 100644 --- a/Makefile +++ b/Makefile @@ -23,12 +23,16 @@ all: \ js/lib/jxon.js \ js/lib/lodash.js \ js/lib/ohauth.js \ + js/lib/rtree.js \ js/lib/sha.js \ js/id/start.js \ js/id/id.js \ js/id/connection.js \ js/id/oauth.js \ js/id/services/*.js \ + data/data.js \ + data/imagery.js \ + data/deprecated.js \ js/id/util.js \ js/id/geo.js \ js/id/geo/*.js \ @@ -40,14 +44,16 @@ all: \ js/id/modes/*.js \ js/id/operations.js \ js/id/operations/*.js \ - js/id/controller/*.js \ - js/id/graph/*.js \ + js/id/core/*.js \ js/id/renderer/*.js \ js/id/svg.js \ js/id/svg/*.js \ js/id/ui.js \ js/id/ui/*.js \ - js/id/end.js + js/id/validate.js \ + js/id/end.js \ + js/lib/locale.js \ + locale/*.js iD.js: Makefile @rm -f $@ diff --git a/README.md b/README.md index 711cb5418..e185fbb4f 100644 --- a/README.md +++ b/README.md @@ -2,31 +2,30 @@ [![Build Status](https://secure.travis-ci.org/systemed/iD.png)](https://travis-ci.org/systemed/iD) -[![](http://ideditor.com/img/editor.png)](http://geowiki.com/iD/) - -[Try the online demo of the most recent code.](http://geowiki.com/iD/) and -[open issues for bugs and ideas!](https://github.com/systemed/iD/issues) +[![](http://ideditor.com/img/editor.png)](http://ideditor.com/) ## Basics * iD is a JavaScript [OpenStreetMap](http://www.openstreetmap.org/) editor. * It's intentionally simple. It lets you do the most basic tasks while not breaking other people's data. -* We support modern browsers. Data is rendered with [d3](http://d3js.org/). +* It supports modern browsers. Data is rendered with [d3](http://d3js.org/). ## Participate! -* [Read NOTES.md, our ongoing dev journal](https://github.com/systemed/iD/blob/master/NOTES.md) -* Fork this project. We eagerly accept pull requests. -* See [open issues in the issue tracker if you're looking for something to do](https://github.com/systemed/iD/issues?state=open) +* [Try out the latest stable release](http://geowiki.com/iD/) +* [Read up on Contributing and the code style of iD](CONTRIBUTING.md) +* See [open issues in the issue tracker](https://github.com/systemed/iD/issues?state=open) if you're looking for something to do -To run the code locally, just fork this project and run it from a local webserver. -With a Mac, you can enable Web Sharing and drop this in your website directory. +## Installation -If you have Python handy, just `cd` into `iD` and run +To run the current development version, fork this project and serve it locally. +If you have Python handy, just `cd` into the project root directory and run python -m SimpleHTTPServer +Or, with a Mac, you can enable Web Sharing and clone iD into your website directory. + Come on in, the water's lovely. More help? Ping RichardF, tmcw, or jfire on IRC (`irc.oftc.net`, in `#osm-dev` or `#osm`), on the OSM mailing lists or at richard@systemeD.net. diff --git a/combobox.html b/combobox.html new file mode 100644 index 000000000..383dbe325 --- /dev/null +++ b/combobox.html @@ -0,0 +1,169 @@ + + + + + iD + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + diff --git a/css/app.css b/css/app.css index f30a4570f..efcdd8b87 100644 --- a/css/app.css +++ b/css/app.css @@ -19,10 +19,11 @@ body { } .limiter { + position: relative; max-width: 1200px; } -div, textarea, input, span, ul, li, ol, a, button { +div, textarea, input, form, span, ul, li, ol, a, button { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; @@ -33,6 +34,7 @@ div, textarea, input, span, ul, li, ol, a, button { a, button, input, textarea { -webkit-tap-highlight-color:rgba(0,0,0,0); -webkit-touch-callout:none; + cursor:url(../img/cursor-pointer.png) 6 1, auto; } h2 { @@ -92,7 +94,6 @@ a:hover { color:#597be7; } - textarea, input[type=text] { background-color: white; @@ -120,7 +121,7 @@ input[type=text]:focus { } input[type=text] { - padding:4px 10px; + padding:5px 10px; height:30px; resize: none; } @@ -181,11 +182,14 @@ ul li { list-style: none;} ul.toggle-list li a { font-weight: bold; color: #333; - padding: 10px; - border-top: 1px solid white; + padding: 5px 10px; display:block; - border-top: 1px solid rgba(0, 0, 0, .5); + border-top: 1px solid #ccc; + white-space:nowrap; + text-overflow:ellipsis; + overflow:hidden; } +ul.toggle-list li a:hover { background-color: #ececec;} ul.toggle-list .icon { float: left; @@ -220,7 +224,7 @@ ul.link-list li:last-child { .fillD { background:rgba(0,0,0,.8); - color: #a9a9a9; + color: #6C6C6C; } @@ -246,7 +250,6 @@ form.hide { button { line-height:20px; - position: relative; border:0; color:#222; background: white; @@ -254,7 +257,6 @@ button { font-size:12px; display: inline-block; height:40px; - cursor:url(../img/cursor-pointer.png) 6 1, auto; border-radius:4px; -webkit-transition: background 100ms; -moz-transition: background 100ms; @@ -265,16 +267,17 @@ button:hover { background-color: #ececec; } -button.col3:hover { - background: #bde5aa; -} - button.active { cursor:url(../img/cursor-pointing.png) 6 1, auto; } -button.active:not([disabled]) { - background: #6bc641; +button.disabled { + background: #6c6c6c; + cursor: auto; +} + +button.active:not([disabled]):not(.disabled) { + background: #7092ff; } button.minor { @@ -283,11 +286,11 @@ button.minor { width: 20px; border: 0; box-shadow: none; - background-color: transparent; + background: rgba(0,0,0,.5); } button.minor:hover { - background: white; + background: #222; } button.centered { @@ -319,6 +322,8 @@ button.centered { border-radius:0 4px 4px 0; } +button.browse .label { display: none;} + button.action { background: #7092ff; } @@ -344,16 +349,15 @@ button.save .count { button.save.has-count .count { display: block; position: absolute; - left: 115%; - top: 0; - bottom: 0; - background: rgba(255,255,255,.5); + top: 5px; + background: rgba(255, 255, 255, .5); color: #333; padding: 10px; height: 30px; line-height: 12px; border-radius: 4px; margin: auto; + margin-left: 8.3333%; } button.save.has-count .count::before { @@ -416,12 +420,12 @@ button[disabled] .label { } /* Definitions for every icon */ -.icon.browse { background-position: 0px -20px;} -.icon.add-point { background-position: -20px -20px;} -.icon.add-line { background-position: -40px -20px;} -.icon.add-area { background-position: -60px -20px;} -.icon.undo { background-position: -80px -20px;} -.icon.redo { background-position: -100px -20px;} +.icon.browse { background-position: 0px 0px;} +.icon.add-point { background-position: -20px 0px;} +.icon.add-line { background-position: -40px 0px;} +.icon.add-area { background-position: -60px 0px;} +.icon.undo { background-position: -80px 0px;} +.icon.redo { background-position: -100px 0px;} .icon.apply { background-position: -120px 0px;} .icon.save { background-position: -140px 0px;} @@ -431,6 +435,7 @@ button[disabled] .label { .icon.inspect { background-position: -220px 0px;} .icon.zoom-in { background-position: -240px 0px;} .icon.zoom-out { background-position: -260px 0px;} +.icon.plus { background-position: -240px 0px;} .icon.geocode { background-position: -280px 0px;} .icon.layers { background-position: -300px 0px;} .icon.avatar { background-position: -320px 0px;} @@ -438,16 +443,7 @@ button[disabled] .label { .icon.geolocate { background-position: -360px 0px;} .icon.warning { background-position: -380px 0px;} -.icon.close-modal{ background-position: -200px -40px;} - -.icon.invert.zoom-in { background-position: -240px -40px;} - -.icon.browse { background-position: 0px 0px;} -.icon.add-point { background-position: -20px 0px;} -.icon.add-line { background-position: -40px 0px;} -.icon.add-area { background-position: -60px 0px;} -.icon.undo { background-position: -80px 0px;} -.icon.redo { background-position: -100px 0px;} +.icon.close-modal { background-position: -200px 0px;} .fillD .icon.avatar { background-position: -320px -20px;} .fillD .icon.nearby { background-position: -340px -20px;} @@ -456,8 +452,8 @@ button[disabled] .icon.browse { background-position: 0px -40px;} button[disabled] .icon.add-point { background-position: -20px -40px;} button[disabled] .icon.add-line { background-position: -40px -40px;} button[disabled] .icon.add-area { background-position: -60px -40px;} -button[disabled] .icon.undo { background-position: -80px -40px;} -button[disabled] .icon.redo { background-position: -100px -40px;} +button.disabled .icon.undo { background-position: -80px -40px;} +button.disabled .icon.redo { background-position: -100px -40px;} button[disabled] .apply.icon { background-position: -120px -40px;} button[disabled] .save.icon { background-position: -140px -40px;} button[disabled] .close.icon { background-position: -160px -40px;} @@ -471,13 +467,24 @@ button[disabled] .icon.layers { background-position: -300px -40px;} button[disabled] .icon.avatar { background-position: -320px -40px;} button[disabled] .icon.nearby { background-position: -340px -40px;} -.icon.big-line { background-position: 0px -80px;} -.icon.big-point { background-position: -40px -80px;} -.icon.big-area { background-position: -80px -80px;} -.icon.big-vertex { background-position: -120px -80px;} -.icon.big-inspect { background-position: -160px -80px;} +.icon.big-line { background-position: 0px -80px;} +.icon.big-point { background-position: -40px -80px;} +.icon.big-area { background-position: -80px -80px;} +.icon.big-vertex { background-position: -120px -80px;} +.icon.big-inspect { background-position: -160px -80px;} .icon.big-relation { background-position: -200px -80px;} +.icon-operation-delete { background-position: 0px -140px;} +.icon-operation-circularize { background-position: -20px -140px;} +.icon-operation-straighten { background-position: -40px -140px;} +.icon-operation-split { background-position: -60px -140px;} +.icon-operation-disconnect { background-position: -80px -140px;} +.icon-operation-reverse { background-position: -100px -140px;} +.icon-operation-move { background-position: -120px -140px;} +.icon-operation-merge { background-position: -140px -140px;} +.icon-operation-orthogonalize { background-position: -160px -140px;} + + /* Toggle icon is special */ .toggle.icon { background-position: 0px -180px;} a:hover .toggle.icon { background-position: -20px -180px;} @@ -609,19 +616,20 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} width: 60%; } -.inspector-inner .add-tag-row { - width: 100%; -} - -.inspector-inner .add-tag-row button { +.inspector-inner .add-tag { + width: 20%; + height: 30px; + border-top: 0; + background: rgba(0,0,0,.5); border-radius: 0 0 4px 4px; } -.inspector-inner .add-tag { - width: 40%; - height: 30px; - border: 1px solid #ccc; - border-top: 0; +.inspector-inner .add-tag:hover { + background: rgba(0,0,0,.8); +} + +.inspector-inner .add-tag .label { + display: none; } /* Map Controls */ @@ -631,25 +639,27 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} position:absolute; } -.map-control button { - width: 40px; - background: rgba(0,0,0,.8); - border-radius: 0 4px 4px 0; +.map-control > button { + width: 30px; + background: rgba(0,0,0,.5); + border-radius: 0; + border-bottom: 1px solid rgba(0, 0, 0, 1); } -.map-control button:hover { - background: rgba(0, 0, 0, .9); +.map-control > button:hover { + background: rgba(0, 0, 0, .8); } -.map-control button.active:hover { - background: #6bc641; +.map-control > button.active:hover { + background: #7092ff; } .map-overlay { - width: 150px; - position:absolute; - left:50px; - top:0; + right: 75%; + max-width: 260px; + min-width: 210px; + position: fixed; + left: 40px; display: block; border-radius: 4px; } @@ -658,43 +668,117 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} .zoombuttons { top:70px; - width: 40px; + width: 30px; } .zoombuttons button.zoom-in { border-radius:0 4px 0 0; - border-bottom: 1px solid rgba(0, 0, 0, .5); } .zoombuttons button.zoom-out { border-top:0; - border-radius:0 0 4px 0; } /* Layer Switcher */ .layerswitcher-control { - top:210px; + top:190px; +} + +.nudge-container { + margin-top: 10px; } .layerswitcher-control .adjustments button { - opacity:0.5; - height:20px; + height:30px; font-size:10px; - font-weight:normal; padding:0 5px 3px 5px; background: white; - border: 1px solid #ddd; - border-radius: 0; + text-transform: uppercase; + font-weight: bold; } .layerswitcher-control .adjustments button:hover { - opacity: 1; + background:#ececec; +} + +.layerswitcher-control .alignment-toggle { + display: block; + padding-left: 12px; + position: relative; +} + +.layerswitcher-control .alignment-toggle:before { + content: ''; + display: block; + position: absolute; + height: 0; + width: 0; + left: 0; + top: 4px; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; + border-left: 8px solid #7092ff; +} + +.layerswitcher-control .alignment-toggle.expanded:before { + border-top: 8px solid #7092ff; + border-bottom: 0; + border-right: 4px solid transparent; + border-left: 4px solid transparent; + } .layerswitcher-control .nudge { - width:20px; - margin-right:2px; + text-indent: -9999px; + overflow: hidden; + width:16.6666%; + border-radius: 0; + border-right: 1px solid rgba(0, 0, 0, .5); + position: relative; +} + +.layerswitcher-control .nudge::after { + content: ''; + display: block; + position: absolute; + margin: auto; + left: 0; right: 0; top: 0; bottom: 0; + height: 0; + width: 0; +} + +.layerswitcher-control .nudge.left::after { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #222; +} + +.layerswitcher-control .nudge.right::after { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #222; +} + +.layerswitcher-control .nudge.top::after { + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-bottom: 5px solid #222; +} + +.layerswitcher-control .nudge.bottom::after { + border-right: 5px solid transparent; + border-left: 5px solid transparent; + border-top: 5px solid #222; +} + +.layerswitcher-control .nudge:first-child { + border-radius: 4px 0 0 4px; +} + +.layerswitcher-control .reset { + width: 33.3333%; + border-radius: 0 4px 4px 0; } .opacity-options-wrapper { @@ -708,6 +792,7 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} position: absolute; right: 10px; top: 10px; + border: 1px solid #ddd; } .opacity-options li { @@ -720,14 +805,14 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} .opacity-options li .select-box{ position: absolute; width:20px; - height:20px; + height:18px; z-index: 9999; } .layerswitcher-control li:hover .select-box, .layerswitcher-control li.selected .select-box { - border: 2px solid #6bc641; - background: rgba(107, 198, 65, .5); + border: 2px solid #7092ff; + background: rgba(89, 123, 231, .5); opacity: .5; } .layerswitcher-control li.selected:hover .select-box, @@ -739,23 +824,45 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} background:#222; display:inline-block; width:20px; - height:20px; + height:18px; } /* Geocoder */ -.geocode-control { - top:160px; +.geocode-control, .geocode-control form { + top:150px; +} + +.geocode-control form { + padding: 4px; } .geocode-control input { - width: 140px; - border: 1px solid #ccc; - margin: 4px; + width: 100%; } +.geocode-control div.content { + z-index: 100; + top: 190px; + max-height: 300px; + overflow-y: auto; +} + +.geocode-control div.content span { + display: inline-block; + border-bottom: 1px solid #333; + padding: 5px 10px; +} + +/* Geolocator */ + .geolocate-control { - top:260px; + top:230px; +} + +.geolocate-control button { + border-radius: 0 0 4px 0; + border-bottom: 0; } /* Map @@ -791,6 +898,10 @@ img.tile { -o-transform-origin:0 0; } +#surface { + position: static; +} + #tile-g { opacity: 0.5; } @@ -819,17 +930,17 @@ img.tile { color:#fff; } -#user-list a:not(:last-child):after { +.user-list a:not(:last-child):after { content: ', '; } /* Account Information */ -.user-container { +.account { float: left; } -.user-container .logout { +.account .logout { margin-left:10px; border-left: 1px solid white; padding-left: 10px; @@ -878,9 +989,11 @@ div.typeahead a:first-child { display: inline-block; position:absolute; width: 50%; - left: 25%; + left: 0; + right: 0; + margin: auto; max-width: 600px; - top:80px; + top: 80px; z-index: 3; } @@ -899,10 +1012,14 @@ div.typeahead a:first-child { position: absolute; right:5px; top:5px; - border:0; + opacity: .5; + -webkit-transition: opacity 100ms; + -moz-transition: opacity 100ms; + transition: opacity 100ms; } .modal button.close-modal:hover { background-color: transparent; + opacity: 1; } .shaded { @@ -920,7 +1037,7 @@ div.typeahead a:first-child { padding: 20px; } -.modal-section.header { +.modal-section:first-child { border-radius: 4px 4px 0 0; } @@ -963,7 +1080,6 @@ div.typeahead a:first-child { .modal-splash { width: 33.3333%; - left: 33.3333%; } .logo { @@ -976,7 +1092,7 @@ div.typeahead a:first-child { /* Commit Modal ------------------------------------------------------- */ -.commit-modal .user-info { +.commit-modal a.user-info { display: inline-block; } @@ -1007,11 +1123,10 @@ div.typeahead a:first-child { border:1px solid #ccc; background:#fff; max-height: 160px; - overflow: visible; } -.commit-modal .warning-section .changeset-list { - margin-right: 20px; +.commit-modal .warning-section .changeset-list button { + float: right; } .commit-section.modal-section { @@ -1022,30 +1137,17 @@ div.typeahead a:first-child { .commit-modal .changeset-list li { position: relative; -} - -.commit-modal .changeset-list li button { - position: absolute; - right: -30px; + border-top:1px solid #ccc; + padding:5px 10px; } .modal-section { padding: 20px; } -.modal-section.header { - border-radius: 4px 4px 0 0; -} - -.modal-section .buttons { - padding-top: 10px; - width: 100%; -} - .modal-section img.wiki-image { - max-width: 400px; + max-width: 100%; max-height: 300px; - padding: 10px; display: block; } @@ -1060,11 +1162,6 @@ div.typeahead a:first-child { display:none; } -.changeset-list li { - border-top:1px solid #ccc; - padding:5px 10px; -} - .changeset-list li span.count { font-size:10px; color:#555; @@ -1082,10 +1179,6 @@ div.typeahead a:first-child { font:normal 12px/20px 'Helvetica Neue', Arial, sans-serif; } -.loading-modal { - text-align: center; -} - /* Success ------------------------------------------------------- */ a.success-action { @@ -1104,7 +1197,8 @@ a.success-action { text-align:center; } -.notice .notice-inner { +.notice .zoom-to { + width:100%; height: 40px; border-radius: 5px; line-height: 40px; @@ -1113,22 +1207,27 @@ a.success-action { opacity: 0.9; } -.notice .notice-inner .zoom-to { - width:40px; - height:40px; +.notice .zoom-to:hover { + background: #d8e1ff; +} + +.notice .zoom-to .icon { + margin-top:10px; margin-right:10px; } +.icon.zoom-in-invert { + background-position: -240px -40px; +} + /* Tooltips ------------------------------------------------------- */ .tooltip { - white-space: normal; + width: 200px; position: absolute; - left: 0; right: 0; margin: auto; z-index: -1000; height: 0; - padding: 5px; opacity: 0; display: block; } @@ -1140,106 +1239,140 @@ a.success-action { } .tooltip.top { - margin-top: -5px; + margin-top: -10px; + text-align: center; } .tooltip.right { - margin-left: 5px; + margin-left: 10px; + text-align: left; } .tooltip.bottom { - margin-top: 5px; + margin-top: 10px; + text-align: center; } .tooltip.left { - margin-left: -5px; + margin-left: -10px; + text-align: right; } .tooltip-inner { - text-align: left; - width: 200px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - padding: 5px 10px; - color: #333; - background-color: white; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; + color: #333; + display: inline-block; + padding: 5px 10px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + background-color: white; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } .tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } .tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top-color: white; - border-width: 5px 5px 0; + bottom: -5px; + left: 50%; + margin-left: -5px; + border-top-color: white; + border-width: 5px 5px 0; } .tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-right-color: white; - border-width: 5px 5px 5px 0; + top: 50%; + left: -5px; + margin-top: -5px; + border-right-color: white; + border-width: 5px 5px 5px 0; } .tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-left-color: white; - border-width: 5px 0 5px 5px; + top: 50%; + right: 5px; + margin-top: -5px; + border-left-color: white; + border-width: 5px 0 5px 5px; } .tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-bottom-color: white; - border-width: 0 5px 5px; + top: -5px; + left: 50%; + margin-left: -5px; + border-bottom-color: white; + border-width: 0 5px 5px; } +.Browse .tooltip { + left: -20px !important; } .Browse .tooltip .tooltip-arrow { - left: 30px; + left: 60px; +} + +.tooltip .keyhint-wrap { + padding: 5px 0 5px 0; } .tooltip .keyhint { - float: right; - background: #eee; + display: block; + color: #222; font-size: 10px; - padding: 0 4px; - background:#aaa; - color:#fff; + padding: 0px 7px; + font-weight: bold; + display: inline-block; border-radius: 2px; - margin-left: 4px; + border: 1px solid #CCC; + position: relative; + z-index: 1; + text-align: left; + clear: both; +} + +.tooltip .keyhint .keyhint-label{ + display: inline-block; +} + +.tooltip .keyhint::after { + content: ""; + position: absolute; + border-radius: 2px; + height: 10px; + width: 100%; + z-index: 0; + bottom: -4px; + left: -1px; + border: 1px solid #CCC; + border-top: 0; } .tail { - pointer-events:none; - position: absolute; - background: rgba(255, 255, 255, 0.7); - max-width: 250px; - margin-top: -15px; - padding: 5px; - -webkit-border-radius: 4px; + pointer-events:none; + position: absolute; + background: rgba(255, 255, 255, 0.7); + max-width: 250px; + margin-top: -15px; + padding: 5px; + -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } +.radial-menu-background { + stroke: #aaa; + stroke-opacity: 0.4; +} + .radial-menu-item { - fill: white; - stroke: black; - stroke-width: 1; + fill: black; cursor:url(../img/cursor-pointer.png) 6 1, auto; } @@ -1257,6 +1390,24 @@ a.success-action { fill: rgba(255,255,255,.5); } +.radial-menu .icon { + pointer-events: none; +} + +.radial-menu-tooltip { + background: rgba(255, 255, 255, 0.8); + padding: 5px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.lasso-box { + fill-opacity:0.2; + fill: #bde5aa; + stroke: #000; + stroke-width: 1; +} /* Media Queries ------------------------------------------------------- */ @@ -1265,5 +1416,44 @@ a.success-action { span.label {display: none;} /* override hide for save button */ .icon.icon-pre-text { margin-right: 0px;} - .save .label { display: block;} + .save .label, .apply .label, .cancel .label { display: block;} +} + +div.combobox { + width:155px; + z-index: 9999; + display: none; + box-shadow: 0 5px 10px 0 rgba(0,0,0,.2); + margin-top: -1px; + background: white; + max-height: 180px; + overflow: auto; + border: 1px solid #ccc; +} + +div.combobox a { + height: 25px; + line-height: 25px; + cursor: pointer; + display: block; + border-top:1px solid #ccc; + background-color: #fff; + padding:1px 4px; + white-space: nowrap; +} + +div.combobox a:hover, +div.combobox a.selected { + background: #e1e8ff; + color: #154dff; +} + +div.combobox a:first-child { + border-top: 0; +} + +div.combobox-carat { + cursor: pointer; + padding:0 5px; + vertical-align:middle; } diff --git a/css/map.css b/css/map.css index 7b3038d6c..43db827fe 100644 --- a/css/map.css +++ b/css/map.css @@ -1,3 +1,26 @@ +/* tiles */ +img.tile { + position:absolute; + transform-origin:0 0; + -ms-transform-origin:0 0; + -webkit-transform-origin:0 0; + -moz-transform-origin:0 0; + -o-transform-origin:0 0; + -webkit-user-select: none; + -webkit-user-drag: none; + -moz-user-drag: none; + + opacity: 0; + + -webkit-transition: opacity 200ms linear; + transition: opacity 200ms linear; + -moz-transition: opacity 200ms linear; +} + +img.tile-loaded { + opacity: 1; +} + /* base styles */ path { fill: none; @@ -9,6 +32,10 @@ g.point circle { fill:#fff; } +g.point image { + pointer-events: none; +} + g.point .shadow { fill: none; pointer-events: all; @@ -16,22 +43,28 @@ g.point .shadow { transition: transform 100ms linear; -moz-transition: fill 100ms linear; } -.behavior-hover g.point.hover .shadow { - fill: #E96666; - fill-opacity: 0.3; +.behavior-hover g.point.hover:not(.selected) .shadow { + fill: #f6634f; + fill-opacity: 0.5; } g.point.selected .shadow { - fill: #E96666; + fill: #f6634f; fill-opacity: 0.7; } +g.point.active, g.point.active * { + pointer-events: none; +} + /* vertices */ g.vertex .fill { fill:white; } + g.vertex .stroke { - stroke:#333; + stroke:black; + stroke-opacity: .5; stroke-width:2; fill:white; } @@ -101,28 +134,40 @@ g.vertex.shared .fill { g.vertex .shadow { fill: none; pointer-events: all; - stroke-width: 10; + stroke-width: 20; -webkit-transition: -webkit-transform 100ms linear; transition: transform 100ms linear; -moz-transition: fill 100ms linear; } -.behavior-hover g.vertex.hover .shadow { - fill: #E96666; +.behavior-hover g.vertex.hover:not(.selected) .shadow { + fill: #f6634f; fill-opacity: 0.3; -} +} g.vertex.selected .shadow { - fill: #E96666; - fill-opacity: 0.7; + fill: #f6634f; + fill-opacity: 0.5; } /* midpoints */ -g.midpoint .fill { - fill:#aaa; +.mode-draw-area g.midpoint, +.mode-draw-line g.midpoint, +.mode-add-area g.midpoint, +.mode-add-line g.midpoint, +.mode-add-point g.midpoint, +.behavior-drag-node g.midpoint { + display: none; } -.behavior-hover g.midpoint .fill.hover { - fill:#fff; - stroke:#000; + +g.midpoint .fill { + fill:#ddd; + stroke:black; + stroke-opacity: .5; + opacity: .5; +} +.behavior-hover g.midpoint .fill.hover:not(.selected) { + fill:white; + opacity: .75; } g.midpoint .shadow { @@ -133,8 +178,8 @@ g.midpoint .shadow { transition: transform 100ms linear; -moz-transition: fill 100ms linear; } -.behavior-hover g.midpoint .shadow.hover { - fill:#E96666; +.behavior-hover g.midpoint .shadow.hover:not(.selected) { + fill:#f6634f; fill-opacity: 0.3; } @@ -146,13 +191,8 @@ path.line { } path.stroke { - stroke: #222; - stroke-width: 2; -} - -path.stroke, -path.casing { - shape-rendering: optimizeSpeed; + stroke: black; + stroke-width: 4; } path.shadow { @@ -161,106 +201,83 @@ path.shadow { -webkit-transition: stroke 100ms linear; } -.behavior-hover path.shadow.hover { - stroke: #E96666; +.behavior-hover path.shadow.hover:not(.selected) { + stroke: #f6634f; stroke-opacity: 0.3; } path.shadow.selected { - stroke: #E96666; + stroke: #f6634f; stroke-opacity: 0.7; } path.area.stroke, -path.multipolygon { +path.line.member-type-multipolygon.stroke { stroke-width:2; - stroke:#fff; } -path.area.fill, -path.multipolygon { - fill:#fff; - fill-opacity:0.3; -} - -path.multipolygon { - fill-rule: evenodd; -} - -path.area.fill.member-type-multipolygon { - fill: none; -} - -path.area.stroke.selected { +path.area.stroke.selected, +path.line.member-type-multipolygon.stroke.selected { stroke-width:4 !important; } -path.area.stroke.tag-natural, -path.multipolygon.tag-natural { - stroke: #ADD6A5; +path.area.stroke { + stroke:#fff; +} +path.area.fill { + fill:#fff; + fill-opacity:0.3; + fill-rule: evenodd; +} + +path.stroke.tag-natural { + stroke: #b6e199; stroke-width:1; } -path.area.fill.tag-natural, -path.multipolygon.tag-natural { - fill: #ADD6A5; +path.fill.tag-natural { + fill: #b6e199; } -path.area.stroke.tag-natural-water, -path.multipolygon.tag-natural-water { - stroke: #6382FF; +path.stroke.tag-natural-water { + stroke: #77d3de; } -path.area.fill.tag-natural-water, -path.multipolygon.tag-natural-water { - fill: #ADBEFF; +path.fill.tag-natural-water { + fill: #77d3de; } -path.area.stroke.tag-building, -path.multipolygon.tag-building { - stroke: #9E176A; +path.stroke.tag-building { + stroke: #e06e5f; stroke-width: 1; } -path.area.fill.tag-building, -path.multipolygon.tag-building { - fill: #ff6ec7; +path.fill.tag-building { + fill: #e06e5f; } -path.area.stroke.tag-landuse, -path.area.stroke.tag-natural-wood, -path.area.stroke.tag-natural-tree, -path.area.stroke.tag-natural-grassland, -path.area.stroke.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { - stroke: #006B34; +path.stroke.tag-landuse, +path.stroke.tag-natural-wood, +path.stroke.tag-natural-tree, +path.stroke.tag-natural-grassland, +path.stroke.tag-leisure-park { + stroke: #8cd05f; stroke-width: 1; } -path.area.fill.tag-landuse, -path.area.fill.tag-natural-wood, -path.area.fill.tag-natural-tree, -path.area.fill.tag-natural-grassland, -path.area.fill.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { - fill: #189E59; +path.fill.tag-landuse, +path.fill.tag-natural-wood, +path.fill.tag-natural-tree, +path.fill.tag-natural-grassland, +path.fill.tag-leisure-park { + fill: #8cd05f; fill-opacity: 0.2; } -path.area.stroke.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { - stroke: #beb267; +path.stroke.tag-amenity-parking { + stroke: #aaa; stroke-width: 1; } -path.area.fill.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { - fill: #edecc0; +path.fill.tag-amenity-parking { + fill: #aaa; } -path.multipolygon.tag-boundary { +path.fill.tag-boundary { fill: none; } @@ -291,56 +308,57 @@ svg[data-zoom="16"] path.stroke.tag-highway { path.stroke.tag-highway-motorway, path.stroke.tag-highway-motorway_link, path.stroke.tag-construction-motorway { - stroke:#809bc0; + stroke:#58a9ed; } + path.casing.tag-highway-motorway, path.casing.tag-highway-motorway_link, path.casing.tag-construction-motorway { - stroke:#506077; + stroke:#2c5476; } path.stroke.tag-highway-trunk, path.stroke.tag-highway-trunk_link, path.stroke.tag-construction-trunk { - stroke:#97d397; + stroke:#8cd05f; } path.casing.tag-highway-trunk, path.casing.tag-highway-trunk_link, path.casing.tag-construction-trunk { - stroke:#477147; + stroke:#46682f; } path.stroke.tag-highway-primary, path.stroke.tag-highway-primary_link, path.stroke.tag-construction-primary { - stroke:#ec989a; + stroke:#e06d5f; } path.casing.tag-highway-primary, path.casing.tag-highway-primary_link, path.casing.tag-construction-primary { - stroke:#8d4346; + stroke:#70372f; } path.stroke.tag-highway-secondary, path.stroke.tag-highway-secondary_link, path.stroke.tag-construction-secondary { - stroke:#fecc8b; + stroke:#eab056; } path.casing.tag-highway-secondary, path.casing.tag-highway-secondary_link, path.casing.tag-construction-secondary { - stroke:#a37b48; + stroke:#75582b; } path.stroke.tag-highway-tertiary, path.stroke.tag-highway-tertiary_link, path.stroke.tag-construction-tertiary { - stroke:#ffffb3; + stroke:#ffff7e; } path.casing.tag-highway-tertiary, path.casing.tag-highway-tertiary_link, path.casing.tag-construction-tertiary { - stroke:#bbb; + stroke:#7f7f3f; } path.stroke.tag-highway-unclassified, @@ -377,7 +395,7 @@ path.stroke.tag-highway-pedestrian { shapeRendering: auto; } path.casing.tag-highway-pedestrian { - stroke:#84C382; + stroke:#8cd05f; stroke-width:6 !important; } @@ -450,17 +468,17 @@ svg[data-zoom="16"] path.casing.tag-highway-bridleway { } path.stroke.tag-highway-footway { - stroke: #996600; + stroke: #ae8681; } path.stroke.tag-highway-cycleway { - stroke: #69f; + stroke: #58a9ed; } path.stroke.tag-highway-bridleway { - stroke: green; + stroke: #e06d5f; } path.stroke.tag-highway-steps { - stroke: #ff6257; + stroke: #81d25c; stroke-width: 4; stroke-linecap: butt; stroke-dasharray: 3, 3; @@ -472,7 +490,7 @@ path.casing.tag-highway-steps { path.casing.tag-bridge-yes { stroke-width: 14; - stroke: #000; + stroke: #333; } path.stroke.tag-highway-construction, @@ -516,12 +534,16 @@ path.casing.tag-railway-subway { /* waterways */ +path.fill.tag-waterway { + fill: #77d3de; +} + path.stroke.tag-waterway { - stroke: #10539a; + stroke: #77d3de; stroke-width: 2; } path.casing.tag-waterway { - stroke: #6AA2FF; + stroke: #77d3de; stroke-width: 4; } @@ -540,11 +562,11 @@ svg[data-zoom="16"] path.casing.tag-waterway-river { } path.stroke.tag-waterway-ditch { - stroke: #10539a; + stroke: #6591ff; stroke-width: 1; } path.casing.tag-waterway-ditch { - stroke: #999692; + stroke: #6591ff; stroke-width: 3; } @@ -573,17 +595,22 @@ path.casing.tag-boundary { path.casing.tag-boundary-protected_area, path.casing.tag-boundary-national_park { - stroke: #4D9849; + stroke: #b0e298; } text { font-size:10px; pointer-events: none; + color: #222; + opacity: 1; } .oneway .textpath { pointer-events: none; + font-size: 7px; + baseline-shift: 2px; + opacity: .7; } text.tag-oneway { @@ -611,15 +638,29 @@ text.pathlabel, text.pointlabel { font-size: 12px; font-weight: bold; - fill: black; + fill: #333; text-anchor: middle; pointer-events: none; } +.layer-halo rect, +.layer-halo path, +.layer-label text { + -webkit-transition: opacity 100ms linear; + transition: opacity 100ms linear; + -moz-transition: opacity 100ms linear; +} + .pathlabel .textpath { dominant-baseline: middle; } +/* Opera doesn't support dominant-baseline. See #715 */ +.opera .pathlabel .textpath { + baseline-shift: -33%; + dominant-baseline: auto; +} + .pointlabel-halo, .linelabel-halo, .arealabel-halo { @@ -628,13 +669,9 @@ text.pointlabel { } -text.area.tag-leisure-park { - font-size: 16px; -} - -text.point.tag-shop, -text.point.tag-amenity { - font-size: 9px; +text.point { + font-size: 10px; + baseline-shift: 2px; } /* Cursors */ @@ -663,9 +700,7 @@ text.point.tag-amenity { } .mode-select .area, -.mode-browse .area, -.mode-select .multipolygon, -.mode-browse .multipolygon { +.mode-browse .area { cursor: url(../img/cursor-select-area.png), pointer; } @@ -678,7 +713,6 @@ text.point.tag-amenity { .vertex:active, .line:active, .area:active, -.multipolygon:active, .midpoint:active, .mode-select .selected { cursor: url(../img/cursor-select-acting.png), pointer; @@ -694,14 +728,16 @@ text.point.tag-amenity { .mode-draw-line .behavior-hover .way, .mode-draw-area .behavior-hover .way, .mode-add-line .behavior-hover .way, -.mode-add-area .behavior-hover .way { +.mode-add-area .behavior-hover .way, +.behavior-drag-node.behavior-hover .way { cursor:url(../img/cursor-draw-connect-line.png) 9 9, auto; } .mode-draw-line .behavior-hover .vertex, .mode-draw-area .behavior-hover .vertex, .mode-add-line .behavior-hover .vertex, -.mode-add-area .behavior-hover .vertex { +.mode-add-area .behavior-hover .vertex, +.behavior-drag-node.behavior-hover .vertex { cursor:url(../img/cursor-draw-connect-vertex.png) 9 9, auto; } @@ -712,16 +748,19 @@ text.point.tag-amenity { /* Modes */ .mode-draw-line .vertex.active, -.mode-draw-area .vertex.active { +.mode-draw-area .vertex.active, +.behavior-drag-node .vertex.active { display: none; } .mode-draw-line .way.active, -.mode-draw-area .way.active { +.mode-draw-area .way.active, +.behavior-drag-node .active { pointer-events: none; } /* Ensure drawing doesn't interact with area fills. */ +.mode-add-point .area, .mode-draw-line .area, .mode-draw-area .area, .mode-add-line .area, diff --git a/data/data.js b/data/data.js new file mode 100644 index 000000000..279b1ea86 --- /dev/null +++ b/data/data.js @@ -0,0 +1 @@ +iD.data = {}; diff --git a/data/deprecated.js b/data/deprecated.js new file mode 100644 index 000000000..53457c9f3 --- /dev/null +++ b/data/deprecated.js @@ -0,0 +1,112 @@ +// from http://wiki.openstreetmap.org/wiki/Deprecated_features +// TODO: deal with deprecated 'class' tag +// does not deal with landuse=wood because of indecision +// we will not care about http://taginfo.openstreetmap.org/tags/bicycle_parking=sheffield +iD.data.deprecated = [ + { + old: { barrier: 'wire_fence' }, + replace: { + barrier: 'fence', + fence_type: 'chain' + } + }, + { + old: { barrier: 'wood_fence' }, + replace: { + barrier: 'fence', + fence_type: 'wood' + } + }, + { + old: { highway: 'ford' }, + replace: { + ford: 'yes' + } + }, + { + old: { highway: 'ford' }, + replace: { + ford: 'yes' + } + }, + { + old: { highway: 'ford' }, + replace: { + ford: 'yes' + } + }, + { + old: { highway: 'stile' }, + replace: { + barrier: 'stile' + } + }, + { + old: { highway: 'incline' }, + replace: { + highway: 'road', + incline: 'up' + } + }, + { + old: { highway: 'incline_steep' }, + replace: { + highway: 'road', + incline: 'up' + } + }, + { + old: { highway: 'unsurfaced' }, + replace: { + highway: 'road', + incline: 'unpaved' + } + }, + { + old: { highway: 'unsurfaced' }, + replace: { + highway: 'road', + incline: 'unpaved' + } + }, + { + old: { landuse: 'wood' }, + replace: { + highway: 'road', + incline: 'unpaved' + } + }, + { + old: { natural: 'marsh' }, + replace: { + natural: 'wetland', + wetland: 'marsh' + } + }, + { + old: { shop: 'organic' }, + replace: { + shop: 'supermarket', + organic: 'only' + } + }, + { + old: { power_source: '*' }, + replace: { + 'generator:source': '$1' + } + }, + { + old: { power_rating: '*' }, + replace: { + 'generator:output': '$1' + } + }, + { + old: { bicycle_parking: 'organic' }, + replace: { + shop: 'supermarket', + organic: 'only' + } + } +]; diff --git a/data/discarded.js b/data/discarded.js new file mode 100644 index 000000000..7a1e4580e --- /dev/null +++ b/data/discarded.js @@ -0,0 +1,10 @@ +// entirely discarded tags +iD.data.discarded = [ + 'tiger:upload_uuid', + 'tiger:tlid', + 'tiger:source', + 'tiger:separated', + 'geobase:datasetName', + 'geobase:uuid', + 'sub_sea:type' +]; diff --git a/data/imagery.js b/data/imagery.js new file mode 100644 index 000000000..5cda27b95 --- /dev/null +++ b/data/imagery.js @@ -0,0 +1,609 @@ +iD.data.imagery = [ + { + "name": "Bing aerial imagery", + "template": "http://ecn.t{t}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z", + "description": "Satellite imagery.", + "scaleExtent": [ + 0, + 20 + ], + "subdomains": [ + "0", + "1", + "2", + "3" + ], + "default": "yes", + "sourcetag": "Bing", + "logo": "bing_maps.png", + "logo_url": "http://www.bing.com/maps", + "terms_url": "http://opengeodata.org/microsoft-imagery-details" + }, + { + "name": "MapBox Satellite", + "template": "http://{t}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{z}/{x}/{y}.png", + "description": "Satellite and aerial imagery", + "scaleExtent": [ + 0, + 16 + ], + "subdomains": [ + "a", + "b", + "c" + ], + "terms_url": "http://mapbox.com/tos/" + }, + { + "name": "OpenStreetMap", + "template": "http://{t}.tile.openstreetmap.org/{z}/{x}/{y}.png", + "description": "The default OpenStreetMap layer.", + "scaleExtent": [ + 0, + 18 + ], + "subdomains": [ + "a", + "b", + "c" + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -124.81, + 24.055 + ], + [ + -66.865, + 49.386 + ] + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -179.754, + 50.858 + ], + [ + -129.899, + 71.463 + ] + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -174.46, + 18.702 + ], + [ + -154.516, + 26.501 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -125.991, + 24.005 + ], + [ + -65.988, + 50.009 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -160.579, + 18.902 + ], + [ + -154.793, + 22.508 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -178.001, + 51.255 + ], + [ + -130.004, + 71.999 + ] + ] + }, + { + "name": " USGS Large Scale Aerial Imagery", + "template": "http://{t}.tile.openstreetmap.us/usgs_large_scale/{z}/{x}/{y}.jpg", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -124.819, + 24.496 + ], + [ + -66.931, + 49.443 + ] + ] + }, + { + "name": "British Columbia bc_mosaic", + "template": "http://{t}.imagery.paulnorman.ca/tiles/bc_mosaic/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c", + "d" + ], + "extent": [ + [ + -123.441, + 48.995 + ], + [ + -121.346, + 50.426 + ] + ], + "sourcetag": "bc_mosaic", + "terms_url": "http://imagery.paulnorman.ca/tiles/about.html" + }, + { + "name": "OS OpenData Streetview", + "template": "http://os.openstreetmap.org/sv/{z}/{x}/{y}.png", + "extent": [ + [ + -8.72, + 49.86 + ], + [ + 1.84, + 60.92 + ] + ], + "sourcetag": "OS_OpenData_StreetView" + }, + { + "name": "OS OpenData Locator", + "template": "http://tiles.itoworld.com/os_locator/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS_OpenData_Locator" + }, + { + "name": "OS 1:25k historic (OSM)", + "template": "http://ooc.openstreetmap.org/os1/{z}/{x}/{y}.jpg", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS 1:25k" + }, + { + "name": "OS 1:25k historic (NLS)", + "template": "http://geo.nls.uk/mapdata2/os/25000/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS 1:25k", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "OS 7th Series historic (OSM)", + "template": "http://ooc.openstreetmap.org/os7/{z}/{x}/{y}.jpg", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS7" + }, + { + "name": "OS 7th Series historic (NLS)", + "template": "http://geo.nls.uk/mapdata2/os/seventh/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS7", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "OS New Popular Edition historic", + "template": "http://ooc.openstreetmap.org/npe/{z}/{x}/{y}.png", + "extent": [ + [ + -5.8, + 49.8 + ], + [ + 1.9, + 55.8 + ] + ], + "sourcetag": "NPE" + }, + { + "name": "OS Scottish Popular historic", + "template": "http://ooc.openstreetmap.org/npescotland/tiles/{z}/{x}/{y}.jpg", + "extent": [ + [ + -7.8, + 54.5 + ], + [ + -1.1, + 61.1 + ] + ], + "sourcetag": "NPE" + }, + { + "name": "Surrey aerial", + "template": "http://gravitystorm.dev.openstreetmap.org/surrey/{z}/{x}/{y}.png", + "extent": [ + [ + -0.856, + 51.071 + ], + [ + 0.062, + 51.473 + ] + ], + "sourcetag": "Surrey aerial" + }, + { + "name": "Haiti - GeoEye Jan 13", + "template": "http://gravitystorm.dev.openstreetmap.org/imagery/haiti/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti GeoEye" + }, + { + "name": "Haiti - GeoEye Jan 13+", + "template": "http://maps.nypl.org/tilecache/1/geoeye/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti GeoEye" + }, + { + "name": "Haiti - DigitalGlobe", + "template": "http://maps.nypl.org/tilecache/1/dg_crisis/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti DigitalGlobe" + }, + { + "name": "Haiti - Street names", + "template": "http://hypercube.telascience.org/tiles/1.0.0/haiti-city/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti streetnames" + }, + { + "name": "NAIP", + "template": "http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/{z}/{x}/{y}.png", + "description": "National Agriculture Imagery Program", + "extent": [ + [ + -125.8, + 24.2 + ], + [ + -62.3, + 49.5 + ] + ], + "sourcetag": "NAIP" + }, + { + "name": "NAIP", + "template": "http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/{z}/{x}/{y}.png", + "description": "National Agriculture Imagery Program", + "extent": [ + [ + -168.5, + 55.3 + ], + [ + -140, + 71.5 + ] + ], + "sourcetag": "NAIP" + }, + { + "name": "Ireland - NLS Historic Maps", + "template": "http://geo.nls.uk/maps/ireland/gsgs4136/{z}/{x}/{y}.png", + "extent": [ + [ + -10.71, + 51.32 + ], + [ + -5.37, + 55.46 + ] + ], + "sourcetag": "NLS Historic Maps", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "Denmark - Fugro Aerial Imagery", + "template": "http://tile.openstreetmap.dk/fugro2005/{z}/{x}/{y}.jpg", + "extent": [ + [ + 7.81, + 54.44 + ], + [ + 15.49, + 57.86 + ] + ], + "sourcetag": "Fugro (2005)" + }, + { + "name": "Denmark - Stevns Kommune", + "template": "http://tile.openstreetmap.dk/stevns/2009/{z}/{x}/{y}.jpg", + "extent": [ + [ + 12.09144, + 55.23403 + ], + [ + 12.47712, + 55.43647 + ] + ], + "sourcetag": "Stevns Kommune (2009)" + }, + { + "name": "Austria - geoimage.at", + "template": "http://geoimage.openstreetmap.at/4d80de696cd562a63ce463a58a61488d/{z}/{x}/{y}.jpg", + "extent": [ + [ + 9.36, + 46.33 + ], + [ + 17.28, + 49.09 + ] + ], + "sourcetag": "geoimage.at" + }, + { + "name": "Russia - Kosmosnimki.ru IRS Satellite", + "template": "http://irs.gis-lab.info/?layers=irs&request=GetTile&z={z}&x={x}&y={y}", + "extent": [ + [ + 19.02, + 40.96 + ], + [ + 77.34, + 70.48 + ] + ], + "sourcetag": "Kosmosnimki.ru IRS" + }, + { + "name": "Belarus - Kosmosnimki.ru SPOT4 Satellite", + "template": "http://irs.gis-lab.info/?layers=spot&request=GetTile&z={z}&x={x}&y={y}", + "extent": [ + [ + 23.16, + 51.25 + ], + [ + 32.83, + 56.19 + ] + ], + "sourcetag": "Kosmosnimki.ru SPOT4" + }, + { + "name": "Australia - Geographic Reference Image", + "template": "http://agri.openstreetmap.org/{z}/{x}/{y}.png", + "extent": [ + [ + 96, + -44 + ], + [ + 168, + -9 + ] + ], + "sourcetag": "AGRI" + }, + { + "name": "Switzerland - Canton Aargau - AGIS 25cm 2011", + "template": "http://tiles.poole.ch/AGIS/OF2011/{z}/{x}/{y}.png", + "extent": [ + [ + 7.69, + 47.13 + ], + [ + 8.48, + 47.63 + ] + ], + "sourcetag": "AGIS OF2011" + }, + { + "name": "Switzerland - Canton Solothurn - SOGIS 2007", + "template": "http://mapproxy.sosm.ch:8080/tiles/sogis2007/EPSG900913/{z}/{x}/{y}.png?origin=nw", + "extent": [ + [ + 7.33, + 47.06 + ], + [ + 8.04, + 47.5 + ] + ], + "sourcetag": "Orthofoto 2007 WMS Solothurn" + }, + { + "name": "Poland - Media-Lab fleet GPS masstracks", + "template": "http://masstracks.media-lab.com.pl/{z}/{x}/{y}.png", + "extent": [ + [ + 14, + 48.9 + ], + [ + 24.2, + 55 + ] + ], + "sourcetag": "masstracks" + }, + { + "name": "South Africa - CD:NGI Aerial", + "template": "http://{t}.aerial.openstreetmap.org.za/ngi-aerial/{z}/{x}/{y}.jpg", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + 17.64, + -34.95 + ], + [ + 32.87, + -22.05 + ] + ], + "sourcetag": "ngi-aerial" + } +]; \ No newline at end of file diff --git a/data/imagery.json b/data/imagery.json new file mode 100644 index 000000000..9cf657886 --- /dev/null +++ b/data/imagery.json @@ -0,0 +1,609 @@ +[ + { + "name": "Bing aerial imagery", + "template": "http://ecn.t{t}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z", + "description": "Satellite imagery.", + "scaleExtent": [ + 0, + 20 + ], + "subdomains": [ + "0", + "1", + "2", + "3" + ], + "default": "yes", + "sourcetag": "Bing", + "logo": "bing_maps.png", + "logo_url": "http://www.bing.com/maps", + "terms_url": "http://opengeodata.org/microsoft-imagery-details" + }, + { + "name": "MapBox Satellite", + "template": "http://{t}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{z}/{x}/{y}.png", + "description": "Satellite and aerial imagery", + "scaleExtent": [ + 0, + 16 + ], + "subdomains": [ + "a", + "b", + "c" + ], + "terms_url": "http://mapbox.com/tos/" + }, + { + "name": "OpenStreetMap", + "template": "http://{t}.tile.openstreetmap.org/{z}/{x}/{y}.png", + "description": "The default OpenStreetMap layer.", + "scaleExtent": [ + 0, + 18 + ], + "subdomains": [ + "a", + "b", + "c" + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -124.81, + 24.055 + ], + [ + -66.865, + 49.386 + ] + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -179.754, + 50.858 + ], + [ + -129.899, + 71.463 + ] + ] + }, + { + "name": " TIGER 2012 Roads Overlay", + "template": "http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -174.46, + 18.702 + ], + [ + -154.516, + 26.501 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -125.991, + 24.005 + ], + [ + -65.988, + 50.009 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -160.579, + 18.902 + ], + [ + -154.793, + 22.508 + ] + ] + }, + { + "name": " USGS Topographic Maps", + "template": "http://{t}.tile.openstreetmap.us/usgs_scanned_topos/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -178.001, + 51.255 + ], + [ + -130.004, + 71.999 + ] + ] + }, + { + "name": " USGS Large Scale Aerial Imagery", + "template": "http://{t}.tile.openstreetmap.us/usgs_large_scale/{z}/{x}/{y}.jpg", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + -124.819, + 24.496 + ], + [ + -66.931, + 49.443 + ] + ] + }, + { + "name": "British Columbia bc_mosaic", + "template": "http://{t}.imagery.paulnorman.ca/tiles/bc_mosaic/{z}/{x}/{y}.png", + "subdomains": [ + "a", + "b", + "c", + "d" + ], + "extent": [ + [ + -123.441, + 48.995 + ], + [ + -121.346, + 50.426 + ] + ], + "sourcetag": "bc_mosaic", + "terms_url": "http://imagery.paulnorman.ca/tiles/about.html" + }, + { + "name": "OS OpenData Streetview", + "template": "http://os.openstreetmap.org/sv/{z}/{x}/{y}.png", + "extent": [ + [ + -8.72, + 49.86 + ], + [ + 1.84, + 60.92 + ] + ], + "sourcetag": "OS_OpenData_StreetView" + }, + { + "name": "OS OpenData Locator", + "template": "http://tiles.itoworld.com/os_locator/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS_OpenData_Locator" + }, + { + "name": "OS 1:25k historic (OSM)", + "template": "http://ooc.openstreetmap.org/os1/{z}/{x}/{y}.jpg", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS 1:25k" + }, + { + "name": "OS 1:25k historic (NLS)", + "template": "http://geo.nls.uk/mapdata2/os/25000/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS 1:25k", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "OS 7th Series historic (OSM)", + "template": "http://ooc.openstreetmap.org/os7/{z}/{x}/{y}.jpg", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS7" + }, + { + "name": "OS 7th Series historic (NLS)", + "template": "http://geo.nls.uk/mapdata2/os/seventh/{z}/{x}/{y}.png", + "extent": [ + [ + -9, + 49.8 + ], + [ + 1.9, + 61.1 + ] + ], + "sourcetag": "OS7", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "OS New Popular Edition historic", + "template": "http://ooc.openstreetmap.org/npe/{z}/{x}/{y}.png", + "extent": [ + [ + -5.8, + 49.8 + ], + [ + 1.9, + 55.8 + ] + ], + "sourcetag": "NPE" + }, + { + "name": "OS Scottish Popular historic", + "template": "http://ooc.openstreetmap.org/npescotland/tiles/{z}/{x}/{y}.jpg", + "extent": [ + [ + -7.8, + 54.5 + ], + [ + -1.1, + 61.1 + ] + ], + "sourcetag": "NPE" + }, + { + "name": "Surrey aerial", + "template": "http://gravitystorm.dev.openstreetmap.org/surrey/{z}/{x}/{y}.png", + "extent": [ + [ + -0.856, + 51.071 + ], + [ + 0.062, + 51.473 + ] + ], + "sourcetag": "Surrey aerial" + }, + { + "name": "Haiti - GeoEye Jan 13", + "template": "http://gravitystorm.dev.openstreetmap.org/imagery/haiti/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti GeoEye" + }, + { + "name": "Haiti - GeoEye Jan 13+", + "template": "http://maps.nypl.org/tilecache/1/geoeye/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti GeoEye" + }, + { + "name": "Haiti - DigitalGlobe", + "template": "http://maps.nypl.org/tilecache/1/dg_crisis/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti DigitalGlobe" + }, + { + "name": "Haiti - Street names", + "template": "http://hypercube.telascience.org/tiles/1.0.0/haiti-city/{z}/{x}/{y}.jpg", + "extent": [ + [ + -74.5, + 17.95 + ], + [ + -71.58, + 20.12 + ] + ], + "sourcetag": "Haiti streetnames" + }, + { + "name": "NAIP", + "template": "http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/{z}/{x}/{y}.png", + "description": "National Agriculture Imagery Program", + "extent": [ + [ + -125.8, + 24.2 + ], + [ + -62.3, + 49.5 + ] + ], + "sourcetag": "NAIP" + }, + { + "name": "NAIP", + "template": "http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/{z}/{x}/{y}.png", + "description": "National Agriculture Imagery Program", + "extent": [ + [ + -168.5, + 55.3 + ], + [ + -140, + 71.5 + ] + ], + "sourcetag": "NAIP" + }, + { + "name": "Ireland - NLS Historic Maps", + "template": "http://geo.nls.uk/maps/ireland/gsgs4136/{z}/{x}/{y}.png", + "extent": [ + [ + -10.71, + 51.32 + ], + [ + -5.37, + 55.46 + ] + ], + "sourcetag": "NLS Historic Maps", + "logo": "icons/logo_nls70-nq8.png", + "logo_url": "http://geo.nls.uk/maps/" + }, + { + "name": "Denmark - Fugro Aerial Imagery", + "template": "http://tile.openstreetmap.dk/fugro2005/{z}/{x}/{y}.jpg", + "extent": [ + [ + 7.81, + 54.44 + ], + [ + 15.49, + 57.86 + ] + ], + "sourcetag": "Fugro (2005)" + }, + { + "name": "Denmark - Stevns Kommune", + "template": "http://tile.openstreetmap.dk/stevns/2009/{z}/{x}/{y}.jpg", + "extent": [ + [ + 12.09144, + 55.23403 + ], + [ + 12.47712, + 55.43647 + ] + ], + "sourcetag": "Stevns Kommune (2009)" + }, + { + "name": "Austria - geoimage.at", + "template": "http://geoimage.openstreetmap.at/4d80de696cd562a63ce463a58a61488d/{z}/{x}/{y}.jpg", + "extent": [ + [ + 9.36, + 46.33 + ], + [ + 17.28, + 49.09 + ] + ], + "sourcetag": "geoimage.at" + }, + { + "name": "Russia - Kosmosnimki.ru IRS Satellite", + "template": "http://irs.gis-lab.info/?layers=irs&request=GetTile&z={z}&x={x}&y={y}", + "extent": [ + [ + 19.02, + 40.96 + ], + [ + 77.34, + 70.48 + ] + ], + "sourcetag": "Kosmosnimki.ru IRS" + }, + { + "name": "Belarus - Kosmosnimki.ru SPOT4 Satellite", + "template": "http://irs.gis-lab.info/?layers=spot&request=GetTile&z={z}&x={x}&y={y}", + "extent": [ + [ + 23.16, + 51.25 + ], + [ + 32.83, + 56.19 + ] + ], + "sourcetag": "Kosmosnimki.ru SPOT4" + }, + { + "name": "Australia - Geographic Reference Image", + "template": "http://agri.openstreetmap.org/{z}/{x}/{y}.png", + "extent": [ + [ + 96, + -44 + ], + [ + 168, + -9 + ] + ], + "sourcetag": "AGRI" + }, + { + "name": "Switzerland - Canton Aargau - AGIS 25cm 2011", + "template": "http://tiles.poole.ch/AGIS/OF2011/{z}/{x}/{y}.png", + "extent": [ + [ + 7.69, + 47.13 + ], + [ + 8.48, + 47.63 + ] + ], + "sourcetag": "AGIS OF2011" + }, + { + "name": "Switzerland - Canton Solothurn - SOGIS 2007", + "template": "http://mapproxy.sosm.ch:8080/tiles/sogis2007/EPSG900913/{z}/{x}/{y}.png?origin=nw", + "extent": [ + [ + 7.33, + 47.06 + ], + [ + 8.04, + 47.5 + ] + ], + "sourcetag": "Orthofoto 2007 WMS Solothurn" + }, + { + "name": "Poland - Media-Lab fleet GPS masstracks", + "template": "http://masstracks.media-lab.com.pl/{z}/{x}/{y}.png", + "extent": [ + [ + 14, + 48.9 + ], + [ + 24.2, + 55 + ] + ], + "sourcetag": "masstracks" + }, + { + "name": "South Africa - CD:NGI Aerial", + "template": "http://{t}.aerial.openstreetmap.org.za/ngi-aerial/{z}/{x}/{y}.jpg", + "subdomains": [ + "a", + "b", + "c" + ], + "extent": [ + [ + 17.64, + -34.95 + ], + [ + 32.87, + -22.05 + ] + ], + "sourcetag": "ngi-aerial" + } +] \ No newline at end of file diff --git a/data/imagery.xml b/data/imagery.xml new file mode 100644 index 000000000..91c52fef0 --- /dev/null +++ b/data/imagery.xml @@ -0,0 +1,233 @@ + + + + Bing aerial imagery + http://ecn.t${0|1|2|3}.tiles.virtualearth.net/tiles/a$quadkey.jpeg?g=587&mkt=en-gb&n=z + microsoft + Bing + http://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial/0,0?zl=1&mapVersion=v1&key=Arzdiw4nlOJzRwOz__qailc8NiR31Tt51dN2D7cm57NrnceZnCpgOkmJhNpGoppU&include=ImageryProviders&output=xml + bing_maps.png + http://www.bing.com/maps + http://opengeodata.org/microsoft-imagery-details + yes + + + MapBox Satellite + http://${a|b|c}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/$z/$x/$y.png + http://mapbox.com/tos/ + + + MapQuest Open Aerial + http://oatile1.mqcdn.com/tiles/1.0.0/sat/$z/$x/$y.jpg + http://developer.mapquest.com/web/products/open/map#terms + + + OSM - Mapnik + http://${a|b|c}.tile.openstreetmap.org/$z/$x/$y.png + + + OSM - OpenCycleMap + http://tile.opencyclemap.org/cycle/$z/$x/$y.png + + + OSM - MapQuest + http://otile1.mqcdn.com/tiles/1.0.0/osm/$z/$x/$y.jpg + + + OSM - Tiger Edited Map + 900913 + http://tiger-osm.mapquest.com/tiles/1.0.0/tiger/$z/$x/$y.png + + + OSM - Tiger Edited Map + 900913 + http://tiger-osm.mapquest.com/tiles/1.0.0/tiger/$z/$x/$y.png + + + OSM - Tiger Edited Map + 900913 + http://tiger-osm.mapquest.com/tiles/1.0.0/tiger/$z/$x/$y.png + + + OSM US TIGER 2012 Roads Overlay + 900913 + http://${a|b|c}.tile.openstreetmap.us/tiger2012_roads_expanded/$z/$x/$y.png + + + OSM US TIGER 2012 Roads Overlay + 900913 + http://${a|b|c}.tile.openstreetmap.us/tiger2012_roads_expanded/$z/$x/$y.png + + + OSM US TIGER 2012 Roads Overlay + 900913 + http://${a|b|c}.tile.openstreetmap.us/tiger2012_roads_expanded/$z/$x/$y.png + + + OSM US USGS Topographic Maps + 900913 + http://${a|b|c}.tile.openstreetmap.us/usgs_scanned_topos/$z/$x/$y.png + + + OSM US USGS Topographic Maps + 900913 + http://${a|b|c}.tile.openstreetmap.us/usgs_scanned_topos/$z/$x/$y.png + + + OSM US USGS Topographic Maps + 900913 + http://${a|b|c}.tile.openstreetmap.us/usgs_scanned_topos/$z/$x/$y.png + + + OSM US USGS Large Scale Aerial Imagery + 900913 + http://${a|b|c}.tile.openstreetmap.us/usgs_large_scale/$z/$x/$y.jpg + + + British Columbia bc_mosaic + 900913 + http://${a|b|c|d}.imagery.paulnorman.ca/tiles/bc_mosaic/$z/$x/$y.png + http://imagery.paulnorman.ca/tiles/about.html + bc_mosaic + + + OS OpenData Streetview + http://os.openstreetmap.org/sv/$z/$x/$y.png + OS_OpenData_StreetView + + + OS OpenData Locator + http://tiles.itoworld.com/os_locator/$z/$x/$y.png + OS_OpenData_Locator + source:name + + + OS 1:25k historic (OSM) + http://ooc.openstreetmap.org/os1/$z/$x/$y.jpg + OS 1:25k + + + OS 1:25k historic (NLS) + tms + http://geo.nls.uk/mapdata2/os/25000/$z/$x/$y.png + icons/logo_nls70-nq8.png + http://geo.nls.uk/maps/ + OS 1:25k + + + OS 7th Series historic (OSM) + http://ooc.openstreetmap.org/os7/$z/$x/$y.jpg + OS7 + + + OS 7th Series historic (NLS) + tms + http://geo.nls.uk/mapdata2/os/seventh/$z/$x/$y.png + icons/logo_nls70-nq8.png + http://geo.nls.uk/maps/ + OS7 + + + OS New Popular Edition historic + http://ooc.openstreetmap.org/npe/$z/$x/$y.png + NPE + + + OS Scottish Popular historic + http://ooc.openstreetmap.org/npescotland/tiles/$z/$x/$y.jpg + NPE + + + Surrey aerial + http://gravitystorm.dev.openstreetmap.org/surrey/$z/$x/$y.png + Surrey aerial + + + Haiti - GeoEye Jan 13 + http://gravitystorm.dev.openstreetmap.org/imagery/haiti/$z/$x/$y.jpg + Haiti GeoEye + + + Haiti - GeoEye Jan 13+ + http://maps.nypl.org/tilecache/1/geoeye/$z/$x/$y.jpg + Haiti GeoEye + + + Haiti - DigitalGlobe + http://maps.nypl.org/tilecache/1/dg_crisis/$z/$x/$y.jpg + Haiti DigitalGlobe + + + Haiti - Street names + http://hypercube.telascience.org/tiles/1.0.0/haiti-city/$z/$x/$y.jpg + Haiti streetnames + + + National Agriculture Imagery Program + http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/$z/$x/$y.png + NAIP + + + National Agriculture Imagery Program + http://cube.telascience.org/tilecache/tilecache.py/NAIP_ALL/$z/$x/$y.png + NAIP + + + Ireland - NLS Historic Maps + tms + NLS Historic Maps + http://geo.nls.uk/maps/ireland/gsgs4136/$z/$x/$y.png + icons/logo_nls70-nq8.png + http://geo.nls.uk/maps/ + + + Denmark - Fugro Aerial Imagery + http://tile.openstreetmap.dk/fugro2005/$z/$x/$y.jpg + Fugro (2005) + + + Denmark - Stevns Kommune + http://tile.openstreetmap.dk/stevns/2009/$z/$x/$y.jpg + Stevns Kommune (2009) + + + Austria - geoimage.at + http://geoimage.openstreetmap.at/4d80de696cd562a63ce463a58a61488d/$z/$x/$y.jpg + geoimage.at + + + Russia - Kosmosnimki.ru IRS Satellite + http://irs.gis-lab.info/?layers=irs&request=GetTile&z=$z&x=$x&y=$y + Kosmosnimki.ru IRS + + + Belarus - Kosmosnimki.ru SPOT4 Satellite + http://irs.gis-lab.info/?layers=spot&request=GetTile&z=$z&x=$x&y=$y + Kosmosnimki.ru SPOT4 + + + Australia - Geographic Reference Image + http://agri.openstreetmap.org/$z/$x/$y.png + AGRI + + + Switzerland - Canton Aargau - AGIS 25cm 2011 + http://tiles.poole.ch/AGIS/OF2011/$z/$x/$y.png + AGIS OF2011 + + + Switzerland - Canton Solothurn - SOGIS 2007 + http://mapproxy.sosm.ch:8080/tiles/sogis2007/EPSG900913/$z/$x/$y.png?origin=nw + Orthofoto 2007 WMS Solothurn + + + Poland - Media-Lab fleet GPS masstracks + http://masstracks.media-lab.com.pl/$z/$x/$y.png + masstracks + + + South Africa - CD:NGI Aerial + http://${a|b|c}.aerial.openstreetmap.org.za/ngi-aerial/$z/$x/$y.jpg + ngi-aerial + + diff --git a/data/imagery_convert.js b/data/imagery_convert.js new file mode 100644 index 000000000..abc5cbef6 --- /dev/null +++ b/data/imagery_convert.js @@ -0,0 +1,87 @@ +var fs = require('fs'), + cheerio = require('cheerio'); + +$ = cheerio.load(fs.readFileSync('imagery.xml')); + +var imagery = []; + +// CENSORSHIP! No, these are just layers that essentially duplicate other layers +// or which have no clear use case. +var censor = { + 'MapQuest Open Aerial': true, + 'OSM - OpenCycleMap': true, + 'OSM - MapQuest': true +}; + +var replace = { + 'OSM - Mapnik': 'OpenStreetMap', + 'National Agriculture Imagery Program': 'NAIP' +}; + +var description = { + 'MapBox Satellite': 'Satellite and aerial imagery', + 'OpenStreetMap': 'The default OpenStreetMap layer.', + 'OSM US TIGER 2012 Roads Overlay': 'Public domain road data from the US Government.', + 'Bing aerial imagery': 'Satellite imagery.', + 'NAIP': 'National Agriculture Imagery Program' +}; + +var scaleExtent = { + 'MapBox Satellite': [0, 16], + 'OpenStreetMap': [0, 18], + 'OSM US TIGER 2012 Roads Overlay': [0, 17], + 'Bing aerial imagery': [0, 20] +}; + +$('set').each(function(i) { + var elem = $(this); + + var im = { + name: $(this).find('name').first().text(), + template: $(this).find('url').first().text() + }; + + // no luck with mapquest servers currently... + if (im.template.match(/mapquest/g)) return; + if (censor[im.name]) return; + + im.name = im.name.replace('OSM US', ''); + + if (replace[im.name]) im.name = replace[im.name]; + + if (description[im.name]) im.description = description[im.name]; + + if (scaleExtent[im.name]) im.scaleExtent = scaleExtent[im.name]; + + var subdomains = []; + + im.template = im.template + .replace('$quadkey', '{u}') + .replace(/\$(\w)/g, function(m) { + return '{' + m[1] + '}'; + }) + .replace(/\$\{([^}.]+)\}/g, function(m) { + subdomains = m.slice(2, m.length - 1).split('|'); + return '{t}'; + }); + + if (subdomains.length) im.subdomains = subdomains; + + if (elem.attr('minlat')) { + im.extent = [ + [+elem.attr('minlon'), + +elem.attr('minlat')], + [+elem.attr('maxlon'), + +elem.attr('maxlat')]]; + } + + ['default', 'sourcetag', 'logo', 'logo_url', 'terms_url'].forEach(function(a) { + if (elem.find(a).length) { + im[a] = elem.find(a).first().text(); + } + }); + imagery.push(im); +}); + +fs.writeFileSync('imagery.json', JSON.stringify(imagery, null, 4)); +fs.writeFileSync('imagery.js', 'iD.data.imagery = ' + JSON.stringify(imagery, null, 4) + ';'); diff --git a/icons/tree.png b/icons/tree.png index 7575bd63b..d88c945d4 100644 Binary files a/icons/tree.png and b/icons/tree.png differ diff --git a/icons/unknown.png b/icons/unknown.png index 404602aa4..03fff0c0e 100644 Binary files a/icons/unknown.png and b/icons/unknown.png differ diff --git a/img/modes.png b/img/modes.png new file mode 100644 index 000000000..6b5ec8603 Binary files /dev/null and b/img/modes.png differ diff --git a/img/operations.png b/img/operations.png new file mode 100644 index 000000000..c1e516b29 Binary files /dev/null and b/img/operations.png differ diff --git a/img/source/radial-menu.svg b/img/source/radial-menu.svg index 7c43c61d8..2fa1e44f9 100644 --- a/img/source/radial-menu.svg +++ b/img/source/radial-menu.svg @@ -50,15 +50,15 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="1" - inkscape:cx="-252.25018" - inkscape:cy="571.71412" + inkscape:zoom="4" + inkscape:cx="53.004316" + inkscape:cy="144.37657" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" - inkscape:window-width="1287" + inkscape:window-width="1280" inkscape:window-height="753" - inkscape:window-x="159" + inkscape:window-x="37" inkscape:window-y="0" inkscape:window-maximized="0" inkscape:snap-bbox="true" @@ -102,6 +102,14 @@ orientation="0,1" position="1052,739" id="guide15758" /> + + @@ -111,7 +119,7 @@ image/svg+xml - + @@ -119,6959 +127,17 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Split way + + + + + + + + + + + + Delete + Circularize + Straighten + + Split + + + + Unjoin + Reverse + + + + + + + + + Move + + + + + + + + + + + + Merge + Orthogonalize + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/source/sprite.svg b/img/source/sprite.svg index 5589fc2d5..a8c29982b 100644 --- a/img/source/sprite.svg +++ b/img/source/sprite.svg @@ -9,7 +9,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="400" + width="420" height="200" id="svg12393" version="1.1" @@ -39,14 +39,14 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" - inkscape:cx="336.77382" - inkscape:cy="126.79999" + inkscape:cx="304.46947" + inkscape:cy="116.03146" inkscape:document-units="px" inkscape:current-layer="layer12" showgrid="false" inkscape:window-width="1280" - inkscape:window-height="756" - inkscape:window-x="119" + inkscape:window-height="700" + inkscape:window-x="48" inkscape:window-y="0" inkscape:window-maximized="0" fit-margin-top="0" @@ -56,7 +56,7 @@ showguides="false" inkscape:guide-bbox="true" inkscape:snap-bbox="true" - inkscape:snap-nodes="true"> + inkscape:snap-nodes="false"> + @@ -181,7 +185,7 @@ image/svg+xml - + @@ -190,11 +194,69 @@ inkscape:groupmode="layer" id="layer1" transform="translate(-25,-62.362183)" - style="display:inline"> + style="display:none"> + + + + + + + + + + + + + + + + + + + style="display:inline;fill:#1a1a1a;fill-opacity:1" + transform="translate(505,-653.36218)"> + style="display:inline;color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;overflow:visible;enable-background:accumulate" /> - - - - - - + - - - - - - - - - - - - - - - - - - + style="opacity:0.50000000000000000;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> - - + style="opacity:0.50000000000000000;fill:#000000;fill-opacity:1;display:inline"> + style="opacity:0.50000000000000000;fill:#000000;fill-opacity:1;display:inline"> - @@ -826,9 +790,9 @@ inkscape:connector-curvature="0" id="path58971" d="m 35,44 c 0,1 0,4 0,4 0,0 0,0.5 -0.5,0.5 -0.5,0 -0.429283,-0.27516 -0.5,-0.5 -0.304688,-0.96875 -0.867187,-2.36459 -1,-3 -0.204595,-0.97885 -0.666667,-1 -1,-1 -1,0 -1,1 -1,1 l 1,4 0,3 C 32,52 31.5,51.5 30.5,50.5 29.945312,49.94531 29.257659,49.7508 28.8125,50.00781 28.377049,50.25922 28.150942,50.89541 28.5,51.5 28.853553,52.11237 32,56 32,56 c 1,1 2,1 4,1 2.666667,0 1,0 3,0 2,0 2.288488,-2.86546 3,-5 1,-3 1.5,-5 1.5,-5 0.113427,-0.42332 -0.04289,-0.846 -0.5,-1 -0.880461,-0.29662 -1.36006,0.35278 -1.5,1 -0.25,1.15625 -0.5,2 -0.5,2 -0.09375,0.31383 0.0013,0.5 -0.5,0.5 C 39.99086,49.5 40,49 40,49 c 0,0 0,-2.66667 0,-4 0,-1 -1,-1 -1,-1 0,0 -1,0 -1,1 0,1 0,1.66667 0,3 0,0 0.01305,0.5 -0.5,0.5 C 36.998673,48.5 37,48 37,48 c 0,0 0,-3 0,-4 0,-1 -1,-1 -1,-1 0,0 -1,0 -1,1 z" - style="opacity:0.5;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8013-4);enable-background:accumulate" /> + style="opacity:0.50000000000000000;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;filter:url(#filter8013-4);enable-background:accumulate" /> + inkscape:connector-curvature="0" /> - - + transform="translate(25,-3.0625001e-6)" /> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/sprite.png b/img/sprite.png index be9ba0fdb..02aadf2a3 100644 Binary files a/img/sprite.png and b/img/sprite.png differ diff --git a/index.html b/index.html index b86d188bb..6941ce008 100644 --- a/index.html +++ b/index.html @@ -9,8 +9,8 @@ - - + + @@ -24,7 +24,6 @@ - @@ -33,20 +32,24 @@ + + + + + - + - @@ -54,15 +57,18 @@ + + - + + @@ -70,32 +76,46 @@ + + + + + + - - - - + + + + + + + + + + - + + - - - + + - + + + @@ -108,35 +128,56 @@ - + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + -
- + + + diff --git a/index_packaged.html b/index_packaged.html index c48a21873..7da953292 100644 --- a/index_packaged.html +++ b/index_packaged.html @@ -16,8 +16,17 @@
diff --git a/js/id/actions/add_way.js b/js/id/actions/add_entity.js similarity index 65% rename from js/id/actions/add_way.js rename to js/id/actions/add_entity.js index 2be062d3a..0011d0fe5 100644 --- a/js/id/actions/add_way.js +++ b/js/id/actions/add_entity.js @@ -1,4 +1,4 @@ -iD.actions.AddWay = function(way) { +iD.actions.AddEntity = function(way) { return function(graph) { return graph.replace(way); }; diff --git a/js/id/actions/add_midpoint.js b/js/id/actions/add_midpoint.js new file mode 100644 index 000000000..c0cb97f59 --- /dev/null +++ b/js/id/actions/add_midpoint.js @@ -0,0 +1,11 @@ +iD.actions.AddMidpoint = function(midpoint, node) { + return function(graph) { + graph = graph.replace(node.move(midpoint.loc)); + + midpoint.ways.forEach(function(way) { + graph = graph.replace(graph.entity(way.id).addNode(node.id, way.index)); + }); + + return graph; + }; +}; diff --git a/js/id/actions/add_node.js b/js/id/actions/add_node.js deleted file mode 100644 index 669a81d49..000000000 --- a/js/id/actions/add_node.js +++ /dev/null @@ -1,6 +0,0 @@ -// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/AddCommand.java -iD.actions.AddNode = function(node) { - return function(graph) { - return graph.replace(node); - }; -}; diff --git a/js/id/actions/add_way_node.js b/js/id/actions/add_vertex.js similarity index 80% rename from js/id/actions/add_way_node.js rename to js/id/actions/add_vertex.js index 3e4fffcfc..279737c05 100644 --- a/js/id/actions/add_way_node.js +++ b/js/id/actions/add_vertex.js @@ -1,5 +1,5 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as -iD.actions.AddWayNode = function(wayId, nodeId, index) { +iD.actions.AddVertex = function(wayId, nodeId, index) { return function(graph) { return graph.replace(graph.entity(wayId).addNode(nodeId, index)); }; diff --git a/js/id/actions/change_entity_tags.js b/js/id/actions/change_tags.js similarity index 71% rename from js/id/actions/change_entity_tags.js rename to js/id/actions/change_tags.js index 96901c2eb..c5972644e 100644 --- a/js/id/actions/change_entity_tags.js +++ b/js/id/actions/change_tags.js @@ -1,4 +1,4 @@ -iD.actions.ChangeEntityTags = function(entityId, tags) { +iD.actions.ChangeTags = function(entityId, tags) { return function(graph) { var entity = graph.entity(entityId); return graph.replace(entity.update({tags: tags})); diff --git a/js/id/actions/circular.js b/js/id/actions/circular.js deleted file mode 100644 index da7965c79..000000000 --- a/js/id/actions/circular.js +++ /dev/null @@ -1,58 +0,0 @@ -iD.actions.Circular = function(wayId, map) { - - var action = function(graph) { - var way = graph.entity(wayId), - nodes = graph.childNodes(way), - tags = {}, key, role; - - var points = nodes.map(function(n) { - return map.projection(n.loc); - }), - centroid = d3.geom.polygon(points).centroid(), - radius = d3.median(points, function(p) { - return iD.geo.dist(centroid, p); - }), - circular_nodes = []; - - for (var i = 0; i < 12; i++) { - circular_nodes.push(iD.Node({ loc: map.projection.invert([ - centroid[0] + Math.cos((i / 12) * Math.PI * 2) * radius, - centroid[1] + Math.sin((i / 12) * Math.PI * 2) * radius]) - })); - } - - circular_nodes.push(circular_nodes[0]); - - for (i = 0; i < nodes.length; i++) { - if (graph.parentWays(nodes[i]).length > 1) { - var closest, closest_dist = Infinity, dist; - for (var j = 0; j < circular_nodes.length; j++) { - dist = iD.geo.dist(circular_nodes[j].loc, nodes[i].loc); - if (dist < closest_dist) { - closest_dist = dist; - closest = j; - } - } - circular_nodes.splice(closest, 1, nodes[i]); - if (closest === 0) circular_nodes.splice(circular_nodes.length - 1, 1, nodes[i]); - else if (closest === circular_nodes.length - 1) circular_nodes.splice(0, 1, nodes[i]); - } else { - graph = graph.remove(nodes[i]); - } - } - - for (i = 0; i < circular_nodes.length; i++) { - graph = graph.replace(circular_nodes[i]); - } - - return graph.replace(way.update({ - nodes: _.pluck(circular_nodes, 'id') - })); - }; - - action.enabled = function(graph) { - return graph.entity(wayId).isClosed(); - }; - - return action; -}; diff --git a/js/id/actions/circularize.js b/js/id/actions/circularize.js new file mode 100644 index 000000000..b74dfd976 --- /dev/null +++ b/js/id/actions/circularize.js @@ -0,0 +1,64 @@ +iD.actions.Circularize = function(wayId, projection, count) { + count = count || 12; + + function closestIndex(nodes, loc) { + var idx, min = Infinity, dist; + for (var i = 0; i < nodes.length; i++) { + dist = iD.geo.dist(nodes[i].loc, loc); + if (dist < min) { + min = dist; + idx = i; + } + } + return idx; + } + + var action = function(graph) { + var way = graph.entity(wayId), + nodes = _.uniq(graph.childNodes(way)), + points = nodes.map(function(n) { return projection(n.loc); }), + centroid = d3.geom.polygon(points).centroid(), + radius = d3.median(points, function(p) { + return iD.geo.dist(centroid, p); + }), + ids = []; + + for (var i = 0; i < count; i++) { + var node, + loc = projection.invert([ + centroid[0] + Math.cos((i / 12) * Math.PI * 2) * radius, + centroid[1] + Math.sin((i / 12) * Math.PI * 2) * radius]); + + if (nodes.length) { + var idx = closestIndex(nodes, loc); + node = nodes[idx]; + nodes.splice(idx, 1); + } else { + node = iD.Node(); + } + + ids.push(node.id); + graph = graph.replace(node.move(loc)); + } + + ids.push(ids[0]); + graph = graph.replace(way.update({nodes: ids})); + + for (i = 0; i < nodes.length; i++) { + graph.parentWays(nodes[i]).forEach(function(parent) { + graph = graph.replace(parent.replaceNode(nodes[i].id, + ids[closestIndex(graph.childNodes(way), nodes[i].loc)])); + }); + + graph = iD.actions.DeleteNode(nodes[i].id)(graph); + } + + return graph; + }; + + action.enabled = function(graph) { + return graph.entity(wayId).isClosed(); + }; + + return action; +}; diff --git a/js/id/actions/connect.js b/js/id/actions/connect.js new file mode 100644 index 000000000..3c69b8a0f --- /dev/null +++ b/js/id/actions/connect.js @@ -0,0 +1,44 @@ +// Connect the ways at the given nodes. +// +// The last node will survive. All other nodes will be replaced with +// the surviving node in parent ways, and then removed. +// +// Tags and relation memberships of of non-surviving nodes are merged +// to the survivor. +// +// This is the inverse of `iD.actions.Disconnect`. +// +// Reference: +// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeNodesAction.as +// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/MergeNodesAction.java +// +iD.actions.Connect = function(nodeIds) { + var action = function(graph) { + var survivor = graph.entity(_.last(nodeIds)); + + for (var i = 0; i < nodeIds.length - 1; i++) { + var node = graph.entity(nodeIds[i]), index; + + graph.parentWays(node).forEach(function(parent) { + graph = graph.replace(parent.replaceNode(node.id, survivor.id)); + }); + + graph.parentRelations(node).forEach(function(parent) { + graph = graph.replace(parent.replaceMember(node, survivor)); + }); + + survivor = survivor.mergeTags(node.tags); + graph = iD.actions.DeleteNode(node.id)(graph); + } + + graph = graph.replace(survivor); + + return graph; + }; + + action.enabled = function(graph) { + return nodeIds.length > 1; + }; + + return action; +}; diff --git a/js/id/actions/delete_multiple.js b/js/id/actions/delete_multiple.js new file mode 100644 index 000000000..cd7c022a1 --- /dev/null +++ b/js/id/actions/delete_multiple.js @@ -0,0 +1,18 @@ +iD.actions.DeleteMultiple = function(ids) { + return function(graph) { + var actions = { + way: iD.actions.DeleteWay, + node: iD.actions.DeleteNode, + relation: iD.actions.DeleteRelation + }; + + ids.forEach(function(id) { + var entity = graph.entity(id); + if (entity) { // It may have been deleted aready. + graph = actions[entity.type](id)(graph); + } + }); + + return graph; + }; +}; diff --git a/js/id/actions/delete_relation.js b/js/id/actions/delete_relation.js new file mode 100644 index 000000000..48c62f1e1 --- /dev/null +++ b/js/id/actions/delete_relation.js @@ -0,0 +1,13 @@ +// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteRelationAction.as +iD.actions.DeleteRelation = function(relationId) { + return function(graph) { + var relation = graph.entity(relationId); + + graph.parentRelations(relation) + .forEach(function(parent) { + graph = graph.replace(parent.removeMember(relationId)); + }); + + return graph.remove(relation); + }; +}; diff --git a/js/id/actions/delete_way.js b/js/id/actions/delete_way.js index 211447228..95888470e 100644 --- a/js/id/actions/delete_way.js +++ b/js/id/actions/delete_way.js @@ -8,7 +8,7 @@ iD.actions.DeleteWay = function(wayId) { graph = graph.replace(parent.removeMember(wayId)); }); - way.nodes.forEach(function (nodeId) { + way.nodes.forEach(function(nodeId) { var node = graph.entity(nodeId); // Circular ways include nodes more than once, so they diff --git a/js/id/actions/deprecate_tags.js b/js/id/actions/deprecate_tags.js new file mode 100644 index 000000000..68b9f9d2e --- /dev/null +++ b/js/id/actions/deprecate_tags.js @@ -0,0 +1,36 @@ +iD.actions.DeprecateTags = function(entityId) { + return function(graph) { + var entity = graph.entity(entityId), + newtags = _.clone(entity.tags), + change = false, + rule; + + // This handles deprecated tags with a single condition + for (var i = 0; i < iD.data.deprecated.length; i++) { + + rule = iD.data.deprecated[i]; + var match = _.pairs(rule.old)[0], + replacements = rule.replace ? _.pairs(rule.replace) : null; + + if (entity.tags[match[0]] && match[1] === '*') { + + var value = entity.tags[match[0]]; + if (replacements && !newtags[replacements[0][0]]) { + newtags[replacements[0][0]] = value; + } + delete newtags[match[0]]; + change = true; + + } else if (entity.tags[match[0]] === match[1]) { + newtags = _.assign({}, rule.replace || {}, _.omit(newtags, match[0])); + change = true; + } + } + + if (change) { + return graph.replace(entity.update({tags: newtags})); + } else { + return graph; + } + }; +}; diff --git a/js/id/actions/unjoin_node.js b/js/id/actions/disconnect.js similarity index 88% rename from js/id/actions/unjoin_node.js rename to js/id/actions/disconnect.js index 39e05a9cf..415730e77 100644 --- a/js/id/actions/unjoin_node.js +++ b/js/id/actions/disconnect.js @@ -1,14 +1,16 @@ -// Unjoin the ways at the given node. +// Disconect the ways at the given node. // // For testing convenience, accepts an ID to assign to the (first) new node. // Normally, this will be undefined and the way will automatically // be assigned a new ID. // +// This is the inverse of `iD.actions.Connect`. +// // Reference: // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/UnjoinNodeAction.as // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/UnGlueAction.java // -iD.actions.UnjoinNode = function(nodeId, newNodeId) { +iD.actions.Disconnect = function(nodeId, newNodeId) { var action = function(graph) { if (!action.enabled(graph)) return graph; diff --git a/js/id/actions/join.js b/js/id/actions/join.js new file mode 100644 index 000000000..d3b353dc5 --- /dev/null +++ b/js/id/actions/join.js @@ -0,0 +1,76 @@ +// Join ways at the end node they share. +// +// This is the inverse of `iD.actions.Split`. +// +// Reference: +// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeWaysAction.as +// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/CombineWayAction.java +// +iD.actions.Join = function(ids) { + var idA = ids[0], + idB = ids[1]; + + function groupEntitiesByGeometry(graph) { + var entities = ids.map(function(id) { return graph.entity(id); }); + return _.extend({line: []}, _.groupBy(entities, function(entity) { return entity.geometry(graph); })); + } + + var action = function(graph) { + var a = graph.entity(idA), + b = graph.entity(idB), + nodes; + + if (a.first() === b.first()) { + // a <-- b ==> c + // Expected result: + // a <-- b <-- c + b = iD.actions.Reverse(idB)(graph).entity(idB); + nodes = b.nodes.slice().concat(a.nodes.slice(1)); + + } else if (a.first() === b.last()) { + // a <-- b <== c + // Expected result: + // a <-- b <-- c + nodes = b.nodes.concat(a.nodes.slice(1)); + + } else if (a.last() === b.first()) { + // a --> b ==> c + // Expected result: + // a --> b --> c + nodes = a.nodes.concat(b.nodes.slice(1)); + + } else if (a.last() === b.last()) { + // a --> b <== c + // Expected result: + // a --> b --> c + b = iD.actions.Reverse(idB)(graph).entity(idB); + nodes = a.nodes.concat(b.nodes.slice().slice(1)); + } + + graph.parentRelations(b).forEach(function(parent) { + graph = graph.replace(parent.replaceMember(b, a)); + }); + + graph = graph.replace(a.mergeTags(b.tags).update({ nodes: nodes })); + graph = iD.actions.DeleteWay(idB)(graph); + + return graph; + }; + + action.enabled = function(graph) { + var geometries = groupEntitiesByGeometry(graph); + + if (ids.length !== 2 || ids.length !== geometries.line.length) + return false; + + var a = graph.entity(idA), + b = graph.entity(idB); + + return a.first() === b.first() || + a.first() === b.last() || + a.last() === b.first() || + a.last() === b.last(); + }; + + return action; +}; diff --git a/js/id/actions/merge.js b/js/id/actions/merge.js new file mode 100644 index 000000000..4c4fb2abc --- /dev/null +++ b/js/id/actions/merge.js @@ -0,0 +1,35 @@ +iD.actions.Merge = function(ids) { + function groupEntitiesByGeometry(graph) { + var entities = ids.map(function(id) { return graph.entity(id); }); + return _.extend({point: [], area: []}, _.groupBy(entities, function(entity) { return entity.geometry(graph); })); + } + + var action = function(graph) { + var geometries = groupEntitiesByGeometry(graph), + area = geometries.area[0], + points = geometries.point; + + points.forEach(function(point) { + area = area.mergeTags(point.tags); + + graph.parentRelations(point).forEach(function(parent) { + graph = graph.replace(parent.replaceMember(point, area)); + }); + + graph = graph.remove(point); + }); + + graph = graph.replace(area); + + return graph; + }; + + action.enabled = function(graph) { + var geometries = groupEntitiesByGeometry(graph); + return geometries.area.length === 1 && + geometries.point.length > 0 && + (geometries.area.length + geometries.point.length) === ids.length; + }; + + return action; +}; diff --git a/js/id/actions/move_way.js b/js/id/actions/move_way.js index 8301539c4..376e31d6e 100644 --- a/js/id/actions/move_way.js +++ b/js/id/actions/move_way.js @@ -1,9 +1,9 @@ iD.actions.MoveWay = function(wayId, delta, projection) { return function(graph) { - return graph.update(function (graph) { + return graph.update(function(graph) { var way = graph.entity(wayId); - _.uniq(way.nodes).forEach(function (id) { + _.uniq(way.nodes).forEach(function(id) { var node = graph.entity(id), start = projection(node.loc), end = projection.invert([start[0] + delta[0], start[1] + delta[1]]); diff --git a/js/id/actions/orthogonalize.js b/js/id/actions/orthogonalize.js new file mode 100644 index 000000000..ada281a95 --- /dev/null +++ b/js/id/actions/orthogonalize.js @@ -0,0 +1,109 @@ +/* + * Based on https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/potlatch2/tools/Quadrilateralise.as + */ + +iD.actions.Orthogonalize = function(wayId, projection) { + var action = function(graph) { + var way = graph.entity(wayId), + nodes = graph.childNodes(way), + points = nodes.map(function(n) { return projection(n.loc); }), + quad_nodes = [], + best, i, j; + + var score = squareness(); + for (i = 0; i < 1000; i++) { + var motions = points.map(stepMap); + for (j = 0; j < motions.length; j++) { + points[j] = addPoints(points[j],motions[j]); + } + var newScore = squareness(); + if (newScore < score) { + best = _.clone(points); + score = newScore; + } + if (score < 1.0e-8) { + break; + } + } + points = best; + + for (i = 0; i < points.length - 1; i++) { + graph = graph.replace(graph.entity(nodes[i].id).move(projection.invert(points[i]))); + } + + return graph; + + function stepMap(b, i, array) { + var a = array[(i - 1 + array.length) % array.length], + c = array[(i + 1) % array.length], + p = subtractPoints(a, b), + q = subtractPoints(c, b); + + var scale = iD.geo.dist(p, [0, 0]) + iD.geo.dist(q, [0, 0]); + p = normalizePoint(p, 1.0); + q = normalizePoint(q, 1.0); + + var dotp = p[0] *q[0] + p[1] *q[1]; + // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270). + if (dotp < -0.707106781186547) { + dotp += 1.0; + } + + return normalizePoint(addPoints(p, q), 0.1 * dotp * scale); + } + + function squareness() { + var g = 0.0; + for (var i = 1; i < points.length - 1; i++) { + var score = scoreOfPoints(points[i - 1], points[i], points[i + 1]); + g += score; + } + var startScore = scoreOfPoints(points[points.length - 1], points[0], points[1]); + var endScore = scoreOfPoints(points[points.length - 2], points[points.length - 1], points[0]); + g += startScore; + g += endScore; + return g; + } + + function scoreOfPoints(a, b, c) { + var p = subtractPoints(a, b), + q = subtractPoints(c, b); + + p = normalizePoint(p, 1.0); + q = normalizePoint(q, 1.0); + + var dotp = p[0] * q[0] + p[1] * q[1]; + // score is constructed so that +1, -1 and 0 are all scored 0, any other angle + // is scored higher. + return 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); + } + + function subtractPoints(a, b) { + return [a[0] - b[0], a[1] - b[1]]; + } + + function addPoints(a, b) { + return [a[0] + b[0], a[1] + b[1]]; + } + + function normalizePoint(point, thickness) { + var vector = [0, 0]; + var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]); + if (length !== 0) { + vector[0] = point[0] / length; + vector[1] = point[1] / length; + } + + vector[0] *= thickness; + vector[1] *= thickness; + + return vector; + } + }; + + action.enabled = function(graph) { + return graph.entity(wayId).isClosed(); + }; + + return action; +}; diff --git a/js/id/actions/reverse_way.js b/js/id/actions/reverse.js similarity index 93% rename from js/id/actions/reverse_way.js rename to js/id/actions/reverse.js index 160017637..f9696ceb1 100644 --- a/js/id/actions/reverse_way.js +++ b/js/id/actions/reverse.js @@ -27,7 +27,7 @@ http://wiki.openstreetmap.org/wiki/Route#Members http://josm.openstreetmap.de/browser/josm/trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java */ -iD.actions.ReverseWay = function(wayId) { +iD.actions.Reverse = function(wayId) { var replacements = [ [/:right$/, ':left'], [/:left$/, ':right'], [/:forward$/, ':backward'], [/:backward$/, ':forward'] @@ -62,8 +62,8 @@ iD.actions.ReverseWay = function(wayId) { tags[reverseKey(key)] = reverseValue(key, way.tags[key]); } - graph.parentRelations(way).forEach(function (relation) { - relation.members.forEach(function (member, index) { + graph.parentRelations(way).forEach(function(relation) { + relation.members.forEach(function(member, index) { if (member.id === way.id && (role = {forward: 'backward', backward: 'forward'}[member.role])) { relation = relation.updateMember({role: role}, index); graph = graph.replace(relation); diff --git a/js/id/actions/split.js b/js/id/actions/split.js new file mode 100644 index 000000000..cf5d56f84 --- /dev/null +++ b/js/id/actions/split.js @@ -0,0 +1,102 @@ +// Split a way at the given node. +// +// This is the inverse of `iD.actions.Join`. +// +// For testing convenience, accepts an ID to assign to the new way. +// Normally, this will be undefined and the way will automatically +// be assigned a new ID. +// +// Reference: +// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as +// +iD.actions.Split = function(nodeId, newWayId) { + function candidateWays(graph) { + var node = graph.entity(nodeId), + parents = graph.parentWays(node); + + return parents.filter(function(parent) { + return parent.isClosed() || + (parent.first() !== nodeId && + parent.last() !== nodeId); + }); + } + + var action = function(graph) { + var wayA = candidateWays(graph)[0], + wayB = iD.Way({id: newWayId, tags: wayA.tags}), + nodesA, + nodesB, + isArea = wayA.isArea(); + + if (wayA.isClosed()) { + var nodes = wayA.nodes.slice(0, -1), + idxA = _.indexOf(nodes, nodeId), + idxB = idxA + Math.floor(nodes.length / 2); + + if (idxB >= nodes.length) { + idxB %= nodes.length; + nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1)); + nodesB = nodes.slice(idxB, idxA + 1); + } else { + nodesA = nodes.slice(idxA, idxB + 1); + nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1)); + } + } else { + var idx = _.indexOf(wayA.nodes, nodeId); + nodesA = wayA.nodes.slice(0, idx + 1); + nodesB = wayA.nodes.slice(idx); + } + + wayA = wayA.update({nodes: nodesA}); + wayB = wayB.update({nodes: nodesB}); + + graph = graph.replace(wayA); + graph = graph.replace(wayB); + + graph.parentRelations(wayA).forEach(function(relation) { + if (relation.isRestriction()) { + var via = relation.memberByRole('via'); + if (via && wayB.contains(via.id)) { + relation = relation.updateMember({id: wayB.id}, relation.memberById(wayA.id).index); + graph = graph.replace(relation); + } + } else { + var role = relation.memberById(wayA.id).role, + last = wayB.last(), + i = relation.memberById(wayA.id).index, + j; + + for (j = 0; j < relation.members.length; j++) { + var entity = graph.entity(relation.members[j].id); + if (entity && entity.type === 'way' && entity.contains(last)) { + break; + } + } + + relation = relation.addMember({id: wayB.id, type: 'way', role: role}, i <= j ? i + 1 : i); + graph = graph.replace(relation); + } + }); + + if (isArea) { + var multipolygon = iD.Relation({ + tags: _.extend({}, wayA.tags, {type: 'multipolygon'}), + members: [ + {id: wayA.id, role: 'outer', type: 'way'}, + {id: wayB.id, role: 'outer', type: 'way'} + ]}); + + graph = graph.replace(multipolygon); + graph = graph.replace(wayA.update({tags: {}})); + graph = graph.replace(wayB.update({tags: {}})); + } + + return graph; + }; + + action.enabled = function(graph) { + return candidateWays(graph).length === 1; + }; + + return action; +}; diff --git a/js/id/actions/split_way.js b/js/id/actions/split_way.js deleted file mode 100644 index 6fbd08595..000000000 --- a/js/id/actions/split_way.js +++ /dev/null @@ -1,69 +0,0 @@ -// Split a way at the given node. -// -// For testing convenience, accepts an ID to assign to the new way. -// Normally, this will be undefined and the way will automatically -// be assigned a new ID. -// -// Reference: -// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as -// -iD.actions.SplitWay = function(nodeId, newWayId) { - function candidateWays(graph) { - var node = graph.entity(nodeId), - parents = graph.parentWays(node); - - return parents.filter(function (parent) { - return parent.first() !== nodeId && - parent.last() !== nodeId; - }) - } - - var action = function(graph) { - if (!action.enabled(graph)) - return graph; - - var way = candidateWays(graph)[0], - idx = _.indexOf(way.nodes, nodeId); - - // Create a 'b' way that contains all of the tags in the second - // half of this way - var newWay = iD.Way({id: newWayId, tags: way.tags, nodes: way.nodes.slice(idx)}); - graph = graph.replace(newWay); - - // Reduce the original way to only contain the first set of nodes - graph = graph.replace(way.update({nodes: way.nodes.slice(0, idx + 1)})); - - graph.parentRelations(way).forEach(function(relation) { - if (relation.isRestriction()) { - var via = relation.memberByRole('via'); - if (via && newWay.contains(via.id)) { - relation = relation.updateMember({id: newWay.id}, relation.memberById(way.id).index); - graph = graph.replace(relation); - } - } else { - var role = relation.memberById(way.id).role, - last = newWay.last(), - i = relation.memberById(way.id).index, - j; - - for (j = 0; j < relation.members.length; j++) { - var entity = graph.entity(relation.members[j].id); - if (entity && entity.type === 'way' && entity.contains(last)) { - break; - } - } - - relation = relation.addMember({id: newWay.id, type: 'way', role: role}, i <= j ? i + 1 : i); - graph = graph.replace(relation); - } - }); - - return graph; - }; - - action.enabled = function(graph) { - return candidateWays(graph).length === 1; - }; - - return action; -}; diff --git a/js/id/behavior/add_way.js b/js/id/behavior/add_way.js index 021377890..19f9437cd 100644 --- a/js/id/behavior/add_way.js +++ b/js/id/behavior/add_way.js @@ -1,26 +1,16 @@ -iD.behavior.AddWay = function(mode) { - var map = mode.map, - history = mode.history, - controller = mode.controller, - event = d3.dispatch('startFromNode', 'startFromWay', 'start'), - draw; - - function add(datum) { - if (datum.type === 'node') { - event.startFromNode(datum); - } else if (datum.type === 'way') { - var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); - event.startFromWay(datum, choice.loc, choice.index); - } else if (datum.midpoint) { - var way = history.graph().entity(datum.way); - event.startFromWay(way, datum.loc, datum.index); - } else { - event.start(map.mouseCoordinates()); - } - } +iD.behavior.AddWay = function(context) { + var event = d3.dispatch('start', 'startFromWay', 'startFromNode'), + draw = iD.behavior.Draw(context); var addWay = function(surface) { - map.fastEnable(false) + draw.on('click', event.start) + .on('clickWay', event.startFromWay) + .on('clickNode', event.startFromNode) + .on('cancel', addWay.cancel) + .on('finish', addWay.cancel); + + context.map() + .fastEnable(false) .minzoom(16) .dblclickEnable(false); @@ -28,25 +18,21 @@ iD.behavior.AddWay = function(mode) { }; addWay.off = function(surface) { - map.fastEnable(true) + context.map() + .fastEnable(true) .minzoom(0) .tail(false); window.setTimeout(function() { - map.dblclickEnable(true); + context.map().dblclickEnable(true); }, 1000); surface.call(draw.off); }; addWay.cancel = function() { - controller.exit(); + context.enter(iD.modes.Browse(context)); }; - draw = iD.behavior.Draw() - .on('add', add) - .on('cancel', addWay.cancel) - .on('finish', addWay.cancel); - return d3.rebind(addWay, event, 'on'); }; diff --git a/js/id/behavior/drag.js b/js/id/behavior/drag.js index d88e8a44a..c1d877d1c 100644 --- a/js/id/behavior/drag.js +++ b/js/id/behavior/drag.js @@ -14,7 +14,7 @@ * Delegation is supported via the `delegate` function. */ -iD.behavior.drag = function () { +iD.behavior.drag = function() { function d3_eventCancel() { d3.event.stopPropagation(); d3.event.preventDefault(); @@ -24,8 +24,7 @@ iD.behavior.drag = function () { origin = null, selector = '', filter = null, - keybinding = d3.keybinding('drag'), - event_, target; + event_, target, surface; event.of = function(thiz, argumentz) { return function(e1) { @@ -50,27 +49,26 @@ iD.behavior.drag = function () { moved = 0; var w = d3.select(window) - .on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); + .on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); if (origin) { offset = origin.apply(target, arguments); - offset = [ offset[0] - origin_[0], offset[1] - origin_[1] ]; + offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; } else { - offset = [ 0, 0 ]; + offset = [0, 0]; } - if (touchId == null) d3_eventCancel(); + if (touchId === null) d3_eventCancel(); function point() { - var p = target.parentNode; - return touchId != null ? d3.touches(p).filter(function (p) { + var p = target.parentNode || surface; + return touchId !== null ? d3.touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3.mouse(p); } function dragmove() { - if (!target.parentNode) return dragend(); var p = point(), dx = p[0] - origin_[0], @@ -103,8 +101,8 @@ iD.behavior.drag = function () { if (d3.event.target === eventTarget) w.on("click.drag", click, true); } - w.on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", null) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", null); + w.on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", null) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", null); } function click() { @@ -137,9 +135,6 @@ iD.behavior.drag = function () { drag.off = function(selection) { selection.on("mousedown.drag" + selector, null) .on("touchstart.drag" + selector, null); - keybinding - .on('⌘+Z', null) - .on('⌃+Z', null); }; drag.delegate = function(_) { @@ -174,11 +169,11 @@ iD.behavior.drag = function () { return drag; }; - keybinding - .on('⌘+Z', drag.cancel) - .on('⌃+Z', drag.cancel); - - d3.select(document).call(keybinding); + drag.surface = function() { + if (!arguments.length) return surface; + surface = arguments[0]; + return drag; + }; return d3.rebind(drag, event, "on"); }; diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js deleted file mode 100644 index 9f73a7115..000000000 --- a/js/id/behavior/drag_midpoint.js +++ /dev/null @@ -1,39 +0,0 @@ -iD.behavior.DragMidpoint = function(mode) { - var history = mode.history, - projection = mode.map.projection, - behavior = iD.behavior.drag() - .delegate(".midpoint") - .origin(function(d) { - return projection(d.loc); - }) - .on('start', function(d) { - var w, nds; - d.node = iD.Node({loc: d.loc}); - var args = [iD.actions.AddNode(d.node)]; - for (var i = 0; i < d.ways.length; i++) { - w = d.ways[i], nds = w.nodes; - for (var j = 0; j < nds.length; j++) { - if ((nds[j] === d.nodes[0] && nds[j + 1] === d.nodes[1]) || - (nds[j] === d.nodes[1] && nds[j + 1] === d.nodes[0])) { - args.push(iD.actions.AddWayNode(w.id, d.node.id, j + 1)); - } - } - } - history.perform.apply(history, args); - var node = d3.selectAll('.node.vertex') - .filter(function(data) { return data.id === d.node.id; }); - behavior.target(node.node(), node.datum()); - - }) - .on('move', function(d) { - d3.event.sourceEvent.stopPropagation(); - history.replace( - iD.actions.MoveNode(d.id, projection.invert(d3.event.point))); - }) - .on('end', function() { - history.replace( - iD.actions.Noop(), - 'added a node to a way'); - }); - return behavior; -}; diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index 084c81895..c16ede85f 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -1,25 +1,164 @@ -iD.behavior.DragNode = function(mode) { - var history = mode.history, - projection = mode.map.projection; +iD.behavior.DragNode = function(context) { + var nudgeInterval, + wasMidpoint, + cancelled; - return iD.behavior.drag() - .delegate(".node") - .origin(function(entity) { - return projection(entity.loc); - }) - .on('start', function() { - history.perform( + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.pan(nudge); + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + + function moveAnnotation(entity) { + return t('operations.move.annotation.' + entity.geometry(context.graph())); + } + + function connectAnnotation(datum) { + return t('operations.connect.annotation.' + datum.geometry(context.graph())); + } + + function origin(entity) { + return context.projection(entity.loc); + } + + function start(entity) { + cancelled = d3.event.sourceEvent.shiftKey; + if (cancelled) return behavior.cancel(); + + context.history() + .on('undone.drag-node', cancel); + + wasMidpoint = entity.type === 'midpoint'; + if (wasMidpoint) { + var midpoint = entity; + entity = iD.Node(); + context.perform(iD.actions.AddMidpoint(midpoint, entity)); + + var vertex = context.surface() + .selectAll('.vertex') + .filter(function(d) { return d.id === entity.id; }); + behavior.target(vertex.node(), entity); + + } else { + context.perform( iD.actions.Noop()); - }) - .on('move', function(entity) { - d3.event.sourceEvent.stopPropagation(); - history.replace( - iD.actions.MoveNode(entity.id, projection.invert(d3.event.point)), - 'moved a node'); - }) - .on('end', function() { - history.replace( + } + + var activeIDs = _.pluck(context.graph().parentWays(entity), 'id'); + activeIDs.push(entity.id); + + context.surface() + .classed('behavior-drag-node', true) + .selectAll('.node, .way') + .filter(function(d) { return activeIDs.indexOf(d.id) >= 0; }) + .classed('active', true); + } + + function datum() { + if (d3.event.sourceEvent.altKey) { + return {}; + } else { + return d3.event.sourceEvent.target.__data__ || {}; + } + } + + function move(entity) { + if (cancelled) return; + d3.event.sourceEvent.stopPropagation(); + + var nudge = edge(d3.event.point, context.map().size()); + if (nudge) startNudge(nudge); + else stopNudge(); + + var loc = context.map().mouseCoordinates(); + + var d = datum(); + if (d.type === 'node' && d.id !== entity.id) { + loc = d.loc; + } else if (d.type === 'way') { + var point = d3.mouse(context.surface().node()), + index = iD.geo.chooseIndex(d, point, context); + if (iD.geo.dist(point, context.projection(index.loc)) < 10) { + loc = index.loc; + } + } + + context.replace(iD.actions.MoveNode(entity.id, loc)); + } + + function end(entity) { + if (cancelled) return; + off(); + + var d = datum(); + if (d.type === 'way') { + var point = d3.mouse(context.surface().node()), + choice = iD.geo.chooseIndex(d, point, context); + if (iD.geo.dist(point, context.projection(choice.loc)) < 10) { + context.replace( + iD.actions.MoveNode(entity.id, choice.loc), + iD.actions.AddVertex(d.id, entity.id, choice.index), + connectAnnotation(d)); + return; + } + } + + if (d.type === 'node' && d.id !== entity.id) { + context.replace( + iD.actions.Connect([entity.id, d.id]), + connectAnnotation(d)); + + } else if (wasMidpoint) { + context.replace( iD.actions.Noop(), - 'moved a node'); - }); + t('operations.add.annotation.vertex')); + + } else { + context.replace( + iD.actions.Noop(), + moveAnnotation(entity)); + } + } + + function off() { + context.history() + .on('undone.drag_node', null); + + context.surface() + .classed('behavior-drag-node', false) + .selectAll('.active') + .classed('active', false); + + stopNudge(); + } + + function cancel() { + off(); + behavior.cancel(); + } + + var behavior = iD.behavior.drag() + .delegate("g.node, g.point, g.midpoint") + .surface(context.surface().node()) + .origin(origin) + .on('start', start) + .on('move', move) + .on('end', end); + + return behavior; }; diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index 4d0153505..6b1b7ff0a 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -1,43 +1,75 @@ -iD.behavior.Draw = function () { - var event = d3.dispatch('move', 'add', 'undo', 'cancel', 'finish'), +iD.behavior.Draw = function(context) { + var event = d3.dispatch('move', 'click', 'clickWay', + 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), - down, surface, hover; + hover = iD.behavior.Hover(), + closeTolerance = 4, + tolerance = 12; function datum() { - if (d3.event.altKey) { - return {}; - } else { - return d3.event.target.__data__ || {}; - } + if (d3.event.altKey) return {}; + else return d3.event.target.__data__ || {}; } function mousedown() { - down = true; - } - function mouseup() { - down = false; + function point() { + var p = target.node().parentNode; + return touchId !== null ? d3.touches(p).filter(function(p) { + return p.identifier === touchId; + })[0] : d3.mouse(p); + } + + var target = d3.select(this), + touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null, + time = +new Date(), + pos = point(); + + target.on('mousemove.draw', null); + + d3.select(window).on('mouseup.draw', function() { + target.on('mousemove.draw', mousemove); + if (iD.geo.dist(pos, point()) < closeTolerance || + (iD.geo.dist(pos, point()) < tolerance && + (+new Date() - time) < 500)) { + click(); + } + if (target.node() === d3.event.target) { + d3.select(window).on('click.draw', function() { + d3.select(window).on('click.draw', null); + d3.event.stopPropagation(); + }); + } + }); } function mousemove() { - if (!down) { - event.move(datum()); - } + event.move(datum()); } function click() { - event.add(datum()); + var d = datum(); + if (d.type === 'way') { + var choice = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context); + event.clickWay(d, choice.loc, choice.index); + + } else if (d.type === 'node') { + event.clickNode(d); + + } else { + event.click(context.map().mouseCoordinates()); + } } function keydown() { if (d3.event.keyCode === d3.keybinding.modifierCodes.alt) { - surface.call(hover.off); + context.uninstall(hover); } } function keyup() { if (d3.event.keyCode === d3.keybinding.modifierCodes.alt) { - surface.call(hover); + context.install(hover); } } @@ -57,8 +89,7 @@ iD.behavior.Draw = function () { } function draw(selection) { - surface = selection; - hover = iD.behavior.Hover(); + context.install(hover); keybinding .on('⌫', backspace) @@ -68,10 +99,7 @@ iD.behavior.Draw = function () { selection .on('mousedown.draw', mousedown) - .on('mouseup.draw', mouseup) - .on('mousemove.draw', mousemove) - .on('click.draw', click) - .call(hover); + .on('mousemove.draw', mousemove); d3.select(document) .call(keybinding) @@ -82,12 +110,13 @@ iD.behavior.Draw = function () { } draw.off = function(selection) { + context.uninstall(hover); + selection .on('mousedown.draw', null) - .on('mouseup.draw', null) - .on('mousemove.draw', null) - .on('click.draw', null) - .call(hover.off); + .on('mousemove.draw', null); + + d3.select(window).on('mouseup.draw', null); d3.select(document) .call(keybinding.off) diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index 63526df7a..a9a34e37c 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -1,160 +1,181 @@ -iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { - var map = mode.map, - history = mode.history, - controller = mode.controller, - event = d3.dispatch('add', 'addHead', 'addTail', 'addNode', 'addWay'), - way = mode.history.graph().entity(wayId), +iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) { + var way = context.entity(wayId), + isArea = way.geometry() === 'area', finished = false, - draw; + annotation = t((way.isDegenerate() ? + 'operations.start.annotation.' : + 'operations.continue.annotation.') + context.geometry(wayId)), + draw = iD.behavior.Draw(context); - var node = iD.Node({loc: map.mouseCoordinates()}), - nodeId = node.id; + var startIndex = typeof index === 'undefined' ? way.nodes.length - 1 : 0, + start = iD.Node({loc: context.graph().entity(way.nodes[startIndex]).loc}), + end = iD.Node({loc: context.map().mouseCoordinates()}), + segment = iD.Way({ + nodes: [start.id, end.id], + tags: _.clone(way.tags) + }); - history[way.isDegenerate() ? 'replace' : 'perform']( - iD.actions.AddNode(node), - iD.actions.AddWayNode(wayId, node.id, index)); - - function move(datum) { - var loc = map.mouseCoordinates(); - - if (datum.type === 'node' || datum.midpoint) { - loc = datum.loc; - } else if (datum.type === 'way') { - loc = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map).loc; - } - - history.replace(iD.actions.MoveNode(nodeId, loc)); + var f = context[way.isDegenerate() ? 'replace' : 'perform']; + if (isArea) { + f(iD.actions.AddEntity(end), + iD.actions.AddVertex(wayId, end.id, index)); + } else { + f(iD.actions.AddEntity(start), + iD.actions.AddEntity(end), + iD.actions.AddEntity(segment)); } - function add(datum) { - if (datum.id === headId) { - event.addHead(datum); - } else if (datum.id === tailId) { - event.addTail(datum); - } else if (datum.type === 'node' && datum.id !== nodeId) { - event.addNode(datum); + function move(datum) { + var loc = context.map().mouseCoordinates(); + + if (datum.id === end.id || datum.id === segment.id) { + context.surface().selectAll('.way, .node') + .filter(function(d) { + return d.id === end.id || d.id === segment.id; + }) + .classed('active', true); + } else if (datum.type === 'node') { + loc = datum.loc; } else if (datum.type === 'way') { - var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); - event.addWay(datum, choice.loc, choice.index); - } else if (datum.midpoint) { - var way = history.graph().entity(datum.way); - event.addWay(way, datum.loc, datum.index); - } else { - event.add(map.mouseCoordinates()); + loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc; } + + context.replace(iD.actions.MoveNode(end.id, loc)); } function undone() { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); + } + + function lineActives(d) { + return d.id === segment.id || d.id === start.id || d.id === end.id; + } + + function areaActives(d) { + return d.id === wayId || d.id === end.id; } var drawWay = function(surface) { - map.fastEnable(false) + draw.on('move', move) + .on('click', drawWay.add) + .on('clickWay', drawWay.addWay) + .on('clickNode', drawWay.addNode) + .on('undo', context.undo) + .on('cancel', drawWay.cancel) + .on('finish', drawWay.finish); + + context.map() + .fastEnable(false) .minzoom(16) .dblclickEnable(false); surface.call(draw) .selectAll('.way, .node') - .filter(function (d) { return d.id === wayId || d.id === nodeId; }) + .filter(isArea ? areaActives : lineActives) .classed('active', true); - history.on('undone.draw', undone); + context.history() + .on('undone.draw', undone); }; drawWay.off = function(surface) { if (!finished) - history.pop(); + context.pop(); - map.fastEnable(true) + context.map() + .fastEnable(true) .minzoom(0) .tail(false); window.setTimeout(function() { - map.dblclickEnable(true); + context.map().dblclickEnable(true); }, 1000); surface.call(draw.off) .selectAll('.way, .node') .classed('active', false); - history.on('undone.draw', null); + context.history() + .on('undone.draw', null); }; function ReplaceTemporaryNode(newNode) { return function(graph) { - return graph - .replace(way.removeNode(nodeId).addNode(newNode.id, index)) - .remove(node); - } + if (isArea) { + return graph + .replace(way.removeNode(end.id).addNode(newNode.id, index)) + .remove(end); + + } else { + return graph + .replace(graph.entity(wayId).addNode(newNode.id, index)) + .remove(end) + .remove(segment) + .remove(start); + } + }; } + // Accept the current position of the temporary node and continue drawing. + drawWay.add = function(loc) { + var newNode = iD.Node({loc: loc}); + + context.replace( + iD.actions.AddEntity(newNode), + ReplaceTemporaryNode(newNode), + annotation); + + finished = true; + context.enter(mode); + }; + + // Connect the way to an existing way. + drawWay.addWay = function(way, loc, wayIndex) { + var newNode = iD.Node({loc: loc}); + + context.perform( + iD.actions.AddEntity(newNode), + iD.actions.AddVertex(way.id, newNode.id, wayIndex), + ReplaceTemporaryNode(newNode), + annotation); + + finished = true; + context.enter(mode); + }; + // Connect the way to an existing node and continue drawing. - drawWay.addNode = function(node, annotation) { - history.perform( + drawWay.addNode = function(node) { + context.perform( ReplaceTemporaryNode(node), annotation); finished = true; - controller.enter(mode); - }; - - // Connect the way to an existing way. - drawWay.addWay = function(way, loc, wayIndex, annotation) { - var newNode = iD.Node({loc: loc}); - - history.perform( - iD.actions.AddNode(newNode), - iD.actions.AddWayNode(way.id, newNode.id, wayIndex), - ReplaceTemporaryNode(newNode), - annotation); - - finished = true; - controller.enter(mode); - }; - - // Accept the current position of the temporary node and continue drawing. - drawWay.add = function(loc, annotation) { - var newNode = iD.Node({loc: loc}); - - history.replace( - iD.actions.AddNode(newNode), - ReplaceTemporaryNode(newNode), - annotation); - - finished = true; - controller.enter(mode); + context.enter(mode); }; // Finish the draw operation, removing the temporary node. If the way has enough // nodes to be valid, it's selected. Otherwise, return to browse mode. drawWay.finish = function() { - history.pop(); + context.pop(); finished = true; - var way = history.graph().entity(wayId); + var way = context.entity(wayId); if (way) { - controller.enter(iD.modes.Select(way, true)); + context.enter(iD.modes.Select(context, [way.id], true)); } else { - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); } }; // Cancel the draw operation and return to browse, deleting everything drawn. drawWay.cancel = function() { - history.perform( + context.perform( d3.functor(baseGraph), - 'cancelled drawing'); + t('operations.cancel_draw.annotation')); finished = true; - controller.enter(iD.modes.Browse()); + context.enter(iD.modes.Browse(context)); }; - draw = iD.behavior.Draw() - .on('move', move) - .on('add', add) - .on('undo', history.undo) - .on('cancel', drawWay.cancel) - .on('finish', drawWay.finish); - - return d3.rebind(drawWay, event, 'on'); + return drawWay; }; diff --git a/js/id/behavior/hash.js b/js/id/behavior/hash.js new file mode 100644 index 000000000..62f7e51b2 --- /dev/null +++ b/js/id/behavior/hash.js @@ -0,0 +1,86 @@ +iD.behavior.Hash = function(context) { + var s0 = null, // cached location.hash + lat = 90 - 1e-8; // allowable latitude range + + var parser = function(map, s) { + var q = iD.util.stringQs(s); + var args = (q.map || '').split("/").map(Number); + if (args.length < 3 || args.some(isNaN)) { + return true; // replace bogus hash + } else if (s !== formatter(map).slice(1)) { + map.centerZoom([args[2], + Math.min(lat, Math.max(-lat, args[1]))], + args[0]); + } + }; + + var formatter = function(map) { + var center = map.center(), + zoom = map.zoom(), + precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); + var q = iD.util.stringQs(location.hash.substring(1)); + return '#' + iD.util.qsString(_.assign(q, { + map: zoom.toFixed(2) + + '/' + center[1].toFixed(precision) + + '/' + center[0].toFixed(precision) + }), true); + }; + + var move = _.throttle(function() { + var s1 = formatter(context.map()); + if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! + }, 500); + + function hashchange() { + if (location.hash === s0) return; // ignore spurious hashchange events + if (parser(context.map(), (s0 = location.hash).substring(1))) { + move(); // replace bogus hash + } + } + + // the hash can declare that the map should select a feature, but it can + // do so before any features are loaded. thus wait for the feature to + // be loaded and then select + function willselect(id) { + context.map().on('drawn.hash', function() { + if (!context.entity(id)) return; + selectoff(); + context.enter(iD.modes.Select(context, [id])); + }); + + context.on('enter.hash', function() { + if (context.mode().id !== 'browse') selectoff(); + }); + } + + function selectoff() { + context.map().on('drawn.hash', null); + } + + function hash() { + context.map() + .on('move.hash', move); + + d3.select(window) + .on('hashchange.hash', hashchange); + + if (location.hash) { + var q = iD.util.stringQs(location.hash.substring(1)); + if (q.id) willselect(q.id); + hashchange(); + if (q.map) hash.hadHash = true; + } + } + + hash.off = function() { + context.map() + .on('move.hash', null); + + d3.select(window) + .on('hashchange.hash', null); + + location.hash = ""; + }; + + return hash; +}; diff --git a/js/id/behavior/hover.js b/js/id/behavior/hover.js index e4d33dd98..0f5786676 100644 --- a/js/id/behavior/hover.js +++ b/js/id/behavior/hover.js @@ -7,20 +7,22 @@ Only one of these elements can have the :hover pseudo-class, but all of them will have the .hover class. */ -iD.behavior.Hover = function () { +iD.behavior.Hover = function() { var hover = function(selection) { selection.classed('behavior-hover', true); - selection.on('mouseover.hover', function () { + function mouseover() { var datum = d3.event.target.__data__; if (datum) { selection.selectAll('*') - .filter(function (d) { return d === datum; }) + .filter(function(d) { return d === datum; }) .classed('hover', true); } - }); + } - selection.on('mouseout.hover', function () { + selection.on('mouseover.hover', mouseover); + + selection.on('mouseout.hover', function() { selection.selectAll('.hover') .classed('hover', false); }); diff --git a/js/id/behavior/lasso.js b/js/id/behavior/lasso.js new file mode 100644 index 000000000..728b8ad15 --- /dev/null +++ b/js/id/behavior/lasso.js @@ -0,0 +1,71 @@ +iD.behavior.Lasso = function(context) { + + var behavior = function(selection) { + + var timeout = null, + // the position of the first mousedown + pos = null, + lasso; + + function mousedown() { + if (d3.event.shiftKey === true) { + + pos = [d3.event.clientX, d3.event.clientY]; + + lasso = iD.ui.Lasso().a(d3.mouse(context.surface().node())); + + context.surface().call(lasso); + + selection + .on('mousemove.lasso', mousemove) + .on('mouseup.lasso', mouseup); + + d3.event.stopPropagation(); + d3.event.preventDefault(); + + } + } + + function mousemove() { + lasso.b(d3.mouse(context.surface().node())); + } + + function normalize(a, b) { + return [ + [Math.min(a[0], b[0]), Math.min(a[1], b[1])], + [Math.max(a[0], b[0]), Math.max(a[1], b[1])]]; + } + + function mouseup() { + + var extent = iD.geo.Extent( + normalize(context.projection.invert(lasso.a()), + context.projection.invert(lasso.b()))); + + lasso.close(); + + selection + .on('mousemove.lasso', null) + .on('mouseup.lasso', null); + + if (d3.event.clientX !== pos[0] || d3.event.clientY !== pos[1]) { + var selected = context.graph().intersects(extent).filter(function (entity) { + return entity.type === 'node'; + }); + + if (selected.length) { + context.enter(iD.modes.Select(context, _.pluck(selected, 'id'))); + } + } + } + + selection + .on('mousedown.lasso', mousedown); + }; + + behavior.off = function(selection) { + selection.on('mousedown.lasso', null); + }; + + return behavior; +}; diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js new file mode 100644 index 000000000..100cfcb78 --- /dev/null +++ b/js/id/behavior/select.js @@ -0,0 +1,35 @@ +iD.behavior.Select = function(context) { + var behavior = function(selection) { + function click() { + var datum = d3.event.target.__data__; + if (!(datum instanceof iD.Entity)) { + if (!d3.event.shiftKey) + context.enter(iD.modes.Browse(context)); + + } else if (!d3.event.shiftKey) { + // Avoid re-entering Select mode with same entity. + if (context.selection().length !== 1 || context.selection()[0] !== datum.id) { + context.enter(iD.modes.Select(context, [datum.id])); + } else { + context.mode().reselect(); + } + } else if (context.selection().indexOf(datum.id) >= 0) { + var selection = _.without(context.selection(), datum.id); + context.enter(selection.length ? + iD.modes.Select(context, selection) : + iD.modes.Browse(context)); + + } else { + context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); + } + } + + selection.on('click.select', click); + }; + + behavior.off = function(selection) { + selection.on('click.select', null); + }; + + return behavior; +}; diff --git a/js/id/connection.js b/js/id/connection.js index 0460b09ab..f15aa80f8 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -1,14 +1,19 @@ -iD.Connection = function() { +iD.Connection = function(context) { var event = d3.dispatch('auth', 'load'), url = 'http://www.openstreetmap.org', connection = {}, user = {}, - version, keys, inflight = {}, loadedTiles = {}, - oauth = iD.OAuth().url(url); + oauth = iD.OAuth(context).url(url), + ndStr = 'nd', + tagStr = 'tag', + memberStr = 'member', + nodeStr = 'node', + wayStr = 'way', + relationStr = 'relation'; function changesetUrl(changesetId) { return url + '/browse/changeset/' + changesetId; @@ -35,96 +40,103 @@ iD.Connection = function() { } function getNodes(obj) { - var nelems = obj.getElementsByTagName('nd'), nodes = new Array(nelems.length); - for (var i = 0, l = nelems.length; i < l; i++) { - nodes[i] = 'n' + nelems[i].attributes.ref.nodeValue; + var elems = obj.getElementsByTagName(ndStr), + nodes = new Array(elems.length); + for (var i = 0, l = elems.length; i < l; i++) { + nodes[i] = 'n' + elems[i].attributes.ref.nodeValue; } return nodes; } function getTags(obj) { - var tags = {}, tagelems = obj.getElementsByTagName('tag'); - for (var i = 0, l = tagelems.length; i < l; i++) { - var item = tagelems[i]; - tags[item.attributes.k.nodeValue] = item.attributes.v.nodeValue; + var elems = obj.getElementsByTagName(tagStr), + tags = {}; + for (var i = 0, l = elems.length; i < l; i++) { + var attrs = elems[i].attributes; + tags[attrs.k.nodeValue] = attrs.v.nodeValue; } return tags; } function getMembers(obj) { - var elems = obj.getElementsByTagName('member'), + var elems = obj.getElementsByTagName(memberStr), members = new Array(elems.length); - for (var i = 0, l = elems.length; i < l; i++) { + var attrs = elems[i].attributes; members[i] = { - id: elems[i].attributes.type.nodeValue[0] + elems[i].attributes.ref.nodeValue, - type: elems[i].attributes.type.nodeValue, - role: elems[i].attributes.role.nodeValue + id: attrs.type.nodeValue[0] + attrs.ref.nodeValue, + type: attrs.type.nodeValue, + role: attrs.role.nodeValue }; } return members; } - function nodeData(obj) { - var o = { type: 'node', tags: getTags(obj) }; - for (var i = 0, l = obj.attributes.length; i < l; i++) { - o[obj.attributes[i].nodeName] = obj.attributes[i].nodeValue; - } - if (o.lon && o.lat) { - o.loc = [parseFloat(o.lon), parseFloat(o.lat)]; - delete o.lon; delete o.lat; - } - o.id = iD.Entity.id.fromOSM('node', o.id); - return new iD.Node(o); - } + var parsers = { + node: function nodeData(obj) { + var attrs = obj.attributes; + return new iD.Node({ + id: iD.Entity.id.fromOSM(nodeStr, attrs.id.nodeValue), + loc: [parseFloat(attrs.lon.nodeValue), parseFloat(attrs.lat.nodeValue)], + version: attrs.version.nodeValue, + changeset: attrs.changeset.nodeValue, + user: attrs.user.nodeValue, + uid: attrs.uid.nodeValue, + visible: attrs.visible.nodeValue, + timestamp: attrs.timestamp.nodeValue, + tags: getTags(obj) + }); + }, - function wayData(obj) { - var o = { type: 'way', nodes: getNodes(obj), - tags: getTags(obj) - }; - for (var i = 0, l = obj.attributes.length; i < l; i++) { - o[obj.attributes[i].nodeName] = obj.attributes[i].nodeValue; - } - o.id = iD.Entity.id.fromOSM('way', o.id); - return new iD.Way(o); - } + way: function wayData(obj) { + var attrs = obj.attributes; + return new iD.Way({ + id: iD.Entity.id.fromOSM(wayStr, attrs.id.nodeValue), + version: attrs.version.nodeValue, + changeset: attrs.changeset.nodeValue, + user: attrs.user.nodeValue, + uid: attrs.uid.nodeValue, + visible: attrs.visible.nodeValue, + timestamp: attrs.timestamp.nodeValue, + tags: getTags(obj), + nodes: getNodes(obj) + }); + }, - function relationData(obj) { - var o = { - type: 'relation', members: getMembers(obj), - tags: getTags(obj) - }; - for (var i = 0, l = obj.attributes.length; i < l; i++) { - o[obj.attributes[i].nodeName] = obj.attributes[i].nodeValue; + relation: function relationData(obj) { + var attrs = obj.attributes; + return new iD.Relation({ + id: iD.Entity.id.fromOSM(relationStr, attrs.id.nodeValue), + version: attrs.version.nodeValue, + changeset: attrs.changeset.nodeValue, + user: attrs.user.nodeValue, + uid: attrs.uid.nodeValue, + visible: attrs.visible.nodeValue, + timestamp: attrs.timestamp.nodeValue, + tags: getTags(obj), + members: getMembers(obj) + }); } - o.id = iD.Entity.id.fromOSM('relation', o.id); - return new iD.Relation(o); - } + }; function parse(dom) { if (!dom || !dom.childNodes) return new Error('Bad request'); - var root = dom.childNodes[0]; - var entities = {}; + + var root = dom.childNodes[0], + children = root.childNodes, + entities = {}; var i, o, l; - for (i = 0, l = root.childNodes.length; i < l; i++) { - switch(root.childNodes[i].nodeName) { - case 'node': - o = nodeData(root.childNodes[i]); - entities[o.id] = o; - break; - case 'way': - o = wayData(root.childNodes[i]); - entities[o.id] = o; - break; - case 'relation': - o = relationData(root.childNodes[i]); - entities[o.id] = o; - break; + for (i = 0, l = children.length; i < l; i++) { + var child = children[i], + parser = parsers[child.nodeName]; + if (parser) { + o = parser(child); + entities[o.id] = o; } } - return iD.Graph(entities); + return entities; } function authenticated() { @@ -186,21 +198,21 @@ iD.Connection = function() { content: JXON.stringify(connection.changesetJXON({ imagery_used: imagery_used.join(';'), comment: comment, - created_by: 'iD ' + (version || '') + created_by: 'iD ' + iD.version })) - }, function (err, changeset_id) { + }, function(err, changeset_id) { if (err) return callback(err); oauth.xhr({ method: 'POST', path: '/api/0.6/changeset/' + changeset_id + '/upload', options: { header: { 'Content-Type': 'text/xml' } }, content: JXON.stringify(connection.osmChangeJXON(user.id, changeset_id, changes)) - }, function (err) { + }, function(err) { if (err) return callback(err); oauth.xhr({ method: 'PUT', path: '/api/0.6/changeset/' + changeset_id + '/close' - }, function (err) { + }, function(err) { callback(err, changeset_id); }); }); @@ -209,13 +221,14 @@ iD.Connection = function() { function userDetails(callback) { function done(err, user_details) { + if (err) return callback(err); var u = user_details.getElementsByTagName('user')[0], img = u.getElementsByTagName('img'), image_url = ''; if (img && img[0].getAttribute('href')) { image_url = img[0].getAttribute('href'); } - callback(connection.user({ + callback(undefined, connection.user({ display_name: u.attributes.display_name.nodeValue, image_url: image_url, id: u.attributes.id.nodeValue @@ -322,12 +335,6 @@ iD.Connection = function() { return oauth.authenticate(done); }; - connection.version = function(_) { - if (!arguments.length) return version; - version = _; - return connection; - }; - connection.bboxFromAPI = bboxFromAPI; connection.changesetUrl = changesetUrl; connection.loadFromURL = loadFromURL; diff --git a/js/id/controller/controller.js b/js/id/controller/controller.js deleted file mode 100644 index 7e74c56d4..000000000 --- a/js/id/controller/controller.js +++ /dev/null @@ -1,27 +0,0 @@ -// A controller holds a single action at a time and calls `.enter` and `.exit` -// to bind and unbind actions. -iD.Controller = function(map, history) { - var event = d3.dispatch('enter', 'exit'); - var controller = { mode: null }; - - controller.enter = function(mode) { - mode.controller = controller; - mode.history = history; - mode.map = map; - - if (controller.mode) { - controller.mode.exit(); - event.exit(controller.mode); - } - - mode.enter(); - controller.mode = mode; - event.enter(mode); - }; - - controller.exit = function() { - controller.enter(iD.modes.Browse()); - }; - - return d3.rebind(controller, event, 'on'); -}; diff --git a/js/id/core/difference.js b/js/id/core/difference.js new file mode 100644 index 000000000..e8faef149 --- /dev/null +++ b/js/id/core/difference.js @@ -0,0 +1,123 @@ +/* + iD.Difference represents the difference between two graphs. + It knows how to calculate the set of entities that were + created, modified, or deleted, and also contains the logic + for recursively extending a difference to the complete set + of entities that will require a redraw, taking into account + child and parent relationships. + */ +iD.Difference = function(base, head) { + var changes = {}, length = 0; + + _.each(head.entities, function(h, id) { + var b = base.entities[id]; + if (h !== b) { + changes[id] = {base: b, head: h}; + length++; + } + }); + + _.each(base.entities, function(b, id) { + var h = head.entities[id]; + if (!changes[id] && h !== b) { + changes[id] = {base: b, head: h}; + length++; + } + }); + + var difference = {}; + + difference.length = function() { + return length; + }; + + difference.changes = function() { + return changes; + }; + + difference.extantIDs = function() { + var result = []; + _.each(changes, function(change, id) { + if (change.head) result.push(id); + }); + return result; + }; + + difference.modified = function() { + var result = []; + _.each(changes, function(change) { + if (change.base && change.head) result.push(change.head); + }); + return result; + }; + + difference.created = function() { + var result = []; + _.each(changes, function(change) { + if (!change.base && change.head) result.push(change.head); + }); + return result; + }; + + difference.deleted = function() { + var result = []; + _.each(changes, function(change) { + if (change.base && !change.head) result.push(change.base); + }); + return result; + }; + + difference.complete = function(extent) { + var result = {}, id, change; + + function addParents(parents) { + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + + if (parent.id in result) + continue; + + result[parent.id] = parent; + addParents(head.parentRelations(parent)); + } + } + + for (id in changes) { + change = changes[id]; + + var h = change.head, + b = change.base, + entity = h || b; + + if (extent && + (!h || !h.intersects(extent, head)) && + (!b || !b.intersects(extent, base))) + continue; + + result[id] = h; + + if (entity.type === 'way') { + var nh = h ? h.nodes : [], + nb = b ? b.nodes : [], + diff; + + diff = _.difference(nh, nb); + for (var i = 0; i < diff.length; i++) { + result[diff[i]] = head.entity(diff[i]); + } + + diff = _.difference(nb, nh); + for (i = 0; i < diff.length; i++) { + result[diff[i]] = head.entity(diff[i]); + } + } + + addParents(head.parentWays(entity)); + addParents(head.parentRelations(entity)); + } + + return result; + }; + + return difference; +}; diff --git a/js/id/graph/entity.js b/js/id/core/entity.js similarity index 70% rename from js/id/graph/entity.js rename to js/id/core/entity.js index f91bb89d9..bd402552e 100644 --- a/js/id/graph/entity.js +++ b/js/id/core/entity.js @@ -9,22 +9,22 @@ iD.Entity = function(attrs) { return (new iD.Entity()).initialize(arguments); }; -iD.Entity.id = function (type) { +iD.Entity.id = function(type) { return iD.Entity.id.fromOSM(type, iD.Entity.id.next[type]--); }; iD.Entity.id.next = {node: -1, way: -1, relation: -1}; -iD.Entity.id.fromOSM = function (type, id) { +iD.Entity.id.fromOSM = function(type, id) { return type[0] + id; }; -iD.Entity.id.toOSM = function (id) { +iD.Entity.id.toOSM = function(id) { return id.slice(1); }; // A function suitable for use as the second argument to d3.selection#data(). -iD.Entity.key = function (entity) { +iD.Entity.key = function(entity) { return entity.id; }; @@ -43,7 +43,6 @@ iD.Entity.prototype = { if (!this.id && this.type) { this.id = iD.Entity.id(this.type); - this._updated = true; } if (iD.debug) { @@ -63,15 +62,21 @@ iD.Entity.prototype = { }, update: function(attrs) { - return iD.Entity(this, attrs, {_updated: true}); + return iD.Entity(this, attrs); }, - created: function() { - return this._updated && this.osmId().charAt(0) === '-'; - }, - - modified: function() { - return this._updated && this.osmId().charAt(0) !== '-'; + mergeTags: function(tags) { + var merged = _.clone(this.tags); + for (var k in tags) { + var t1 = merged[k], + t2 = tags[k]; + if (t1 && t1 !== t2) { + merged[k] = t1 + "; " + t2; + } else { + merged[k] = t2; + } + } + return this.update({tags: merged}); }, intersects: function(extent, resolver) { @@ -79,7 +84,7 @@ iD.Entity.prototype = { }, hasInterestingTags: function() { - return _.keys(this.tags).some(function (key) { + return _.keys(this.tags).some(function(key) { return key != "attribution" && key != "created_by" && key != "source" && @@ -88,6 +93,23 @@ iD.Entity.prototype = { }); }, + deprecatedTags: function() { + var tags = _.pairs(this.tags); + var deprecated = {}; + + iD.data.deprecated.forEach(function(d) { + var match = _.pairs(d.old)[0]; + tags.forEach(function(t) { + if (t[0] == match[0] && + (t[1] == match[1] || match[1] == '*')) { + deprecated[t[0]] = t[1]; + } + }); + }); + + return deprecated; + }, + friendlyName: function() { // Generate a string such as 'river' or 'Fred's House' for an entity. if (!this.tags || !Object.keys(this.tags).length) { return ''; } diff --git a/js/id/core/graph.js b/js/id/core/graph.js new file mode 100644 index 000000000..a595f0df6 --- /dev/null +++ b/js/id/core/graph.js @@ -0,0 +1,285 @@ +iD.Graph = function(other, mutable) { + if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable); + + if (other instanceof iD.Graph) { + var base = other.base(); + this.entities = _.assign(Object.create(base.entities), other.entities); + this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays); + this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels); + this.inherited = true; + + } else { + if (_.isArray(other)) { + var entities = {}; + for (var i = 0; i < other.length; i++) { + entities[other[i].id] = other[i]; + } + other = entities; + } + this.entities = Object.create({}); + this._parentWays = Object.create({}); + this._parentRels = Object.create({}); + this.rebase(other || {}); + } + + this.transients = {}; + this._childNodes = {}; + this.getEntity = _.bind(this.entity, this); + + if (!mutable) { + this.freeze(); + } +}; + +iD.Graph.prototype = { + entity: function(id) { + return this.entities[id]; + }, + + transient: function(entity, key, fn) { + var id = entity.id, + transients = this.transients[id] || + (this.transients[id] = {}); + + if (transients[key] !== undefined) { + return transients[key]; + } + + return transients[key] = fn.call(entity); + }, + + parentWays: function(entity) { + return _.map(this._parentWays[entity.id], this.getEntity); + }, + + isPoi: function(entity) { + var parentWays = this._parentWays[entity.id]; + return !parentWays || parentWays.length === 0; + }, + + isShared: function(entity) { + var parentWays = this._parentWays[entity.id]; + return parentWays && parentWays.length > 1; + }, + + parentRelations: function(entity) { + return _.map(this._parentRels[entity.id], this.getEntity); + }, + + childNodes: function(entity) { + if (this._childNodes[entity.id]) + return this._childNodes[entity.id]; + + var nodes = []; + for (var i = 0, l = entity.nodes.length; i < l; i++) { + nodes[i] = this.entity(entity.nodes[i]); + } + + return (this._childNodes[entity.id] = nodes); + }, + + base: function() { + return { + 'entities': iD.util.getPrototypeOf(this.entities), + 'parentWays': iD.util.getPrototypeOf(this._parentWays), + 'parentRels': iD.util.getPrototypeOf(this._parentRels) + }; + }, + + // Unlike other graph methods, rebase mutates in place. This is because it + // is used only during the history operation that merges newly downloaded + // data into each state. To external consumers, it should appear as if the + // graph always contained the newly downloaded data. + rebase: function(entities) { + var base = this.base(), + i, k, child, id, keys; + + // Merging of data only needed if graph is the base graph + if (!this.inherited) { + for (i in entities) { + if (!base.entities[i]) { + base.entities[i] = entities[i]; + this._updateCalculated(undefined, entities[i], + base.parentWays, base.parentRels); + } + } + } + + keys = Object.keys(this._parentWays); + for (i = 0; i < keys.length; i++) { + child = keys[i]; + if (base.parentWays[child]) { + for (k = 0; k < base.parentWays[child].length; k++) { + id = base.parentWays[child][k]; + if (!this.entities.hasOwnProperty(id) && !_.contains(this._parentWays[child], id)) { + this._parentWays[child].push(id); + } + } + } + } + + keys = Object.keys(this._parentRels); + for (i = 0; i < keys.length; i++) { + child = keys[i]; + if (base.parentRels[child]) { + for (k = 0; k < base.parentRels[child].length; k++) { + id = base.parentRels[child][k]; + if (!this.entities.hasOwnProperty(id) && !_.contains(this._parentRels[child], id)) { + this._parentRels[child].push(id); + } + } + } + } + }, + + // Updates calculated properties (parentWays, parentRels) for the specified change + _updateCalculated: function(oldentity, entity, parentWays, parentRels) { + + parentWays = parentWays || this._parentWays; + parentRels = parentRels || this._parentRels; + + var type = entity && entity.type || oldentity && oldentity.type, + removed, added, ways, rels, i; + + + if (type === 'way') { + + // Update parentWays + if (oldentity && entity) { + removed = _.difference(oldentity.nodes, entity.nodes); + added = _.difference(entity.nodes, oldentity.nodes); + } else if (oldentity) { + removed = oldentity.nodes; + added = []; + } else if (entity) { + removed = []; + added = entity.nodes; + } + for (i = 0; i < removed.length; i++) { + parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id); + } + for (i = 0; i < added.length; i++) { + ways = _.without(parentWays[added[i]], entity.id); + ways.push(entity.id); + parentWays[added[i]] = ways; + } + } else if (type === 'node') { + + } else if (type === 'relation') { + + // Update parentRels + if (oldentity && entity) { + removed = _.difference(oldentity.members, entity.members); + added = _.difference(entity.members, oldentity); + } else if (oldentity) { + removed = oldentity.members; + added = []; + } else if (entity) { + removed = []; + added = entity.members; + } + for (i = 0; i < removed.length; i++) { + parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id); + } + for (i = 0; i < added.length; i++) { + rels = _.without(parentRels[added[i].id], entity.id); + rels.push(entity.id); + parentRels[added[i].id] = rels; + } + } + }, + + replace: function(entity) { + if (this.entities[entity.id] === entity) + return this; + + return this.update(function() { + this._updateCalculated(this.entities[entity.id], entity); + this.entities[entity.id] = entity; + }); + }, + + remove: function(entity) { + return this.update(function() { + this._updateCalculated(entity, undefined); + this.entities[entity.id] = undefined; + }); + }, + + update: function() { + var graph = this.frozen ? iD.Graph(this, true) : this; + + for (var i = 0; i < arguments.length; i++) { + arguments[i].call(graph, graph); + } + + return this.frozen ? graph.freeze() : this; + }, + + freeze: function() { + this.frozen = true; + + if (iD.debug) { + Object.freeze(this.entities); + } + + return this; + }, + + // get all objects that intersect an extent. + intersects: function(extent) { + var items = []; + for (var i in this.entities) { + var entity = this.entities[i]; + if (entity && this.hasAllChildren(entity) && entity.intersects(extent, this)) { + items.push(entity); + } + } + return items; + }, + + hasAllChildren: function(entity) { + // we're only checking changed entities, since we assume fetched data + // must have all children present + if (this.entities.hasOwnProperty(entity.id)) { + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + if (!this.entities[entity.nodes[i]]) return false; + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + if (!this.entities[entity.members[i].id]) return false; + } + } + } + return true; + }, + + // Obliterates any existing entities + load: function(entities) { + + var base = this.base(), + i, entity, prefix; + this.entities = Object.create(base.entities); + + for (i in entities) { + entity = entities[i]; + prefix = i[0]; + + if (entity === 'undefined') { + this.entities[i] = undefined; + } else if (prefix == 'n') { + this.entities[i] = new iD.Node(entity); + + } else if (prefix == 'w') { + this.entities[i] = new iD.Way(entity); + + } else if (prefix == 'r') { + this.entities[i] = new iD.Relation(entity); + } + this._updateCalculated(base.entities[i], this.entities[i]); + } + return this; + } + +}; diff --git a/js/id/core/history.js b/js/id/core/history.js new file mode 100644 index 000000000..bfd0d5a8c --- /dev/null +++ b/js/id/core/history.js @@ -0,0 +1,245 @@ +iD.History = function(context) { + var stack, index, + imagery_used = 'Bing', + dispatch = d3.dispatch('change', 'undone', 'redone'), + lock = false; + + function perform(actions) { + actions = Array.prototype.slice.call(actions); + + var annotation; + + if (!_.isFunction(_.last(actions))) { + annotation = actions.pop(); + } + + var graph = stack[index].graph; + for (var i = 0; i < actions.length; i++) { + graph = actions[i](graph); + } + + return { + graph: graph, + annotation: annotation, + imagery_used: imagery_used + }; + } + + function change(previous) { + var difference = iD.Difference(previous, history.graph()); + dispatch.change(difference); + return difference; + } + + function getKey(n) { + return 'iD_' + window.location.origin + '_' + n; + } + + var history = { + graph: function() { + return stack[index].graph; + }, + + merge: function(entities) { + for (var i = 0; i < stack.length; i++) { + stack[i].graph.rebase(entities); + } + + dispatch.change(); + }, + + perform: function() { + var previous = stack[index].graph; + + stack = stack.slice(0, index + 1); + stack.push(perform(arguments)); + index++; + + return change(previous); + }, + + replace: function() { + var previous = stack[index].graph; + + // assert(index == stack.length - 1) + stack[index] = perform(arguments); + + return change(previous); + }, + + pop: function() { + var previous = stack[index].graph; + + if (index > 0) { + index--; + stack.pop(); + return change(previous); + } + }, + + undo: function() { + var previous = stack[index].graph; + + // Pop to the first annotated state. + while (index > 0) { + if (stack[index].annotation) break; + index--; + } + + // Pop to the next annotated state. + while (index > 0) { + index--; + if (stack[index].annotation) break; + } + + dispatch.undone(); + return change(previous); + }, + + redo: function() { + var previous = stack[index].graph; + + while (index < stack.length - 1) { + index++; + if (stack[index].annotation) break; + } + + dispatch.redone(); + return change(previous); + }, + + undoAnnotation: function() { + var i = index; + while (i >= 0) { + if (stack[i].annotation) return stack[i].annotation; + i--; + } + }, + + redoAnnotation: function() { + var i = index + 1; + while (i <= stack.length - 1) { + if (stack[i].annotation) return stack[i].annotation; + i++; + } + }, + + difference: function() { + var base = stack[0].graph, + head = stack[index].graph; + return iD.Difference(base, head); + }, + + changes: function() { + var difference = history.difference(); + + function discardTags(entity) { + if (_.isEmpty(entity.tags)) { + return entity; + } else { + return entity.update({ + tags: _.omit(entity.tags, iD.data.discarded) + }); + } + } + + return { + modified: difference.modified().map(discardTags), + created: difference.created().map(discardTags), + deleted: difference.deleted() + }; + }, + + hasChanges: function() { + return this.difference().length() > 0; + }, + + numChanges: function() { + return this.difference().length(); + }, + + imagery_used: function(source) { + if (source) imagery_used = source; + else return _.without( + _.unique(_.pluck(stack.slice(1, index + 1), 'imagery_used')), + undefined, 'Custom'); + }, + + reset: function() { + stack = [{graph: iD.Graph()}]; + index = 0; + dispatch.change(); + }, + + save: function() { + if (!lock) return; + context.storage(getKey('lock'), null); + + if (stack.length <= 1) return; + + var json = JSON.stringify(stack.map(function(i) { + return { + annotation: i.annotation, + imagery_used: i.imagery_used, + entities: i.graph.entities + }; + }), function includeUndefined(key, value) { + if (typeof value === 'undefined') return 'undefined'; + return value; + }); + + context.storage(getKey('history'), json); + context.storage(getKey('nextIDs'), JSON.stringify(iD.Entity.id.next)); + context.storage(getKey('index'), index); + }, + + clearSaved: function() { + if (!lock) return; + context.storage(getKey('history'), null); + context.storage(getKey('nextIDs'), null); + context.storage(getKey('index'), null); + }, + + lock: function() { + if (context.storage(getKey('lock'))) return false; + context.storage(getKey('lock'), true); + lock = true; + return lock; + }, + + restorableChanges: function() { + return lock && !!context.storage(getKey('history')); + }, + + load: function() { + if (!lock) return; + + var json = context.storage(getKey('history')), + nextIDs = context.storage(getKey('nextIDs')), + index_ = context.storage(getKey('index')); + + if (!json) return; + if (nextIDs) iD.Entity.id.next = JSON.parse(nextIDs); + if (index_ !== null) index = parseInt(index_, 10); + + context.storage(getKey('history', null)); + context.storage(getKey('nextIDs', null)); + context.storage(getKey('index', null)); + + stack = JSON.parse(json).map(function(d, i) { + d.graph = iD.Graph(stack[0].graph).load(d.entities); + return d; + }); + stack[0].graph.inherited = false; + dispatch.change(); + + }, + + _getKey: getKey + + }; + + history.reset(); + + return d3.rebind(history, dispatch, 'on'); +}; diff --git a/js/id/graph/node.js b/js/id/core/node.js similarity index 95% rename from js/id/graph/node.js rename to js/id/core/node.js index b9a171752..76be848fa 100644 --- a/js/id/graph/node.js +++ b/js/id/core/node.js @@ -12,7 +12,7 @@ _.extend(iD.Node.prototype, { type: "node", extent: function() { - return iD.geo.Extent(this.loc); + return new iD.geo.Extent(this.loc); }, geometry: function(graph) { @@ -47,6 +47,6 @@ _.extend(iD.Node.prototype, { type: 'Point', coordinates: this.loc } - } + }; } }); diff --git a/js/id/graph/relation.js b/js/id/core/relation.js similarity index 68% rename from js/id/graph/relation.js rename to js/id/core/relation.js index 1ed9e8ff1..778568917 100644 --- a/js/id/graph/relation.js +++ b/js/id/core/relation.js @@ -14,7 +14,7 @@ _.extend(iD.Relation.prototype, { extent: function(resolver) { return resolver.transient(this, 'extent', function() { - return this.members.reduce(function (extent, member) { + return this.members.reduce(function(extent, member) { member = resolver.entity(member.id); if (member) { return extent.extend(member.extent(resolver)); @@ -26,7 +26,7 @@ _.extend(iD.Relation.prototype, { }, geometry: function() { - return 'relation'; + return this.isMultipolygon() ? 'area' : 'relation'; }, // Return the first member with the given role. A copy of the member object @@ -49,6 +49,16 @@ _.extend(iD.Relation.prototype, { } }, + // Return the first member with the given id and role. A copy of the member object + // is returned, extended with an 'index' property whose value is the member index. + memberByIdAndRole: function(id, role) { + for (var i = 0; i < this.members.length; i++) { + if (this.members[i].id === id && this.members[i].role === role) { + return _.extend({}, this.members[i], {index: i}); + } + } + }, + addMember: function(member, index) { var members = this.members.slice(); members.splice(index === undefined ? members.length : index, 0, member); @@ -66,6 +76,28 @@ _.extend(iD.Relation.prototype, { return this.update({members: members}); }, + // Wherever a member appears with id `needle.id`, replace it with a member + // with id `replacement.id`, type `replacement.type`, and the original role, + // unless a member already exists with that id and role. Return an updated + // relation. + replaceMember: function(needle, replacement) { + if (!this.memberById(needle.id)) + return this; + + var members = []; + + for (var i = 0; i < this.members.length; i++) { + var member = this.members[i]; + if (member.id !== needle.id) { + members.push(member); + } else if (!this.memberByIdAndRole(replacement.id, member.role)) { + members.push({id: replacement.id, type: replacement.type, role: member.role}); + } + } + + return this.update({members: members}); + }, + asJXON: function(changeset_id) { var r = { relation: { @@ -83,6 +115,31 @@ _.extend(iD.Relation.prototype, { return r; }, + asGeoJSON: function(resolver) { + if (this.isMultipolygon()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'MultiPolygon', + coordinates: this.multipolygon(resolver) + } + }; + } else { + return { + type: 'FeatureCollection', + properties: this.tags, + features: this.members.map(function(member) { + return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver)); + }) + }; + } + }, + + isMultipolygon: function() { + return this.tags.type === 'multipolygon'; + }, + isRestriction: function() { return !!(this.tags.type && this.tags.type.match(/^restriction:?/)); }, @@ -99,8 +156,8 @@ _.extend(iD.Relation.prototype, { // multipolygon: function(resolver) { var members = this.members - .filter(function (m) { return m.type === 'way' && resolver.entity(m.id); }) - .map(function (m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.childNodes(resolver.entity(m.id)) }; }); + .filter(function(m) { return m.type === 'way' && resolver.entity(m.id); }) + .map(function(m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.childNodes(resolver.entity(m.id)) }; }); function join(ways) { var joined = [], current, first, last, i, how, what; @@ -145,30 +202,28 @@ _.extend(iD.Relation.prototype, { } } - return joined; + return joined.map(function(nodes) { return _.pluck(nodes, 'loc'); }); } function findOuter(inner) { var o, outer; - inner = _.pluck(inner, 'loc'); - for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonIntersectsPolygon(outer, inner)) return o; } } - var outers = join(members.filter(function (m) { return m.role === 'outer'; })), - inners = join(members.filter(function (m) { return m.role === 'inner'; })), - result = outers.map(function (o) { return [o]; }); + var outers = join(members.filter(function(m) { return m.role === 'outer'; })), + inners = join(members.filter(function(m) { return m.role === 'inner'; })), + result = outers.map(function(o) { return [o]; }); for (var i = 0; i < inners.length; i++) { var o = findOuter(inners[i]); diff --git a/js/id/graph/way.js b/js/id/core/way.js similarity index 63% rename from js/id/graph/way.js rename to js/id/core/way.js index aae89a6ce..51590350b 100644 --- a/js/id/graph/way.js +++ b/js/id/core/way.js @@ -14,7 +14,7 @@ _.extend(iD.Way.prototype, { extent: function(resolver) { return resolver.transient(this, 'extent', function() { - return this.nodes.reduce(function (extent, id) { + return this.nodes.reduce(function(extent, id) { return extent.extend(resolver.entity(id).extent(resolver)); }, iD.geo.Extent()); }); @@ -47,11 +47,14 @@ _.extend(iD.Way.prototype, { // - doesn't have area=no // - doesn't have highway tag isArea: function() { - return this.tags.area === 'yes' || - (this.isClosed() && - this.tags.area !== 'no' && - !this.tags.highway && - !this.tags.barrier); + if (this.tags.area === 'yes') + return true; + if (!this.isClosed() || this.tags.area === 'no') + return false; + for (var key in this.tags) + if (key in iD.Way.areaKeys) + return true; + return false; }, isDegenerate: function() { @@ -74,6 +77,19 @@ _.extend(iD.Way.prototype, { return this.update({nodes: nodes}); }, + replaceNode: function(needle, replacement) { + if (this.nodes.indexOf(needle) < 0) + return this; + + var nodes = this.nodes.slice(); + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === needle) { + nodes[i] = replacement; + } + } + return this.update({nodes: nodes}); + }, + removeNode: function(id) { var nodes = _.without(this.nodes, id); @@ -103,13 +119,28 @@ _.extend(iD.Way.prototype, { }, asGeoJSON: function(resolver) { - return { - type: 'Feature', - properties: this.tags, - geometry: { - type: 'LineString', - coordinates: _.pluck(resolver.childNodes(this), 'loc') - } - }; + if (this.isArea()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'Polygon', + coordinates: [_.pluck(resolver.childNodes(this), 'loc')] + } + }; + } else { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'LineString', + coordinates: _.pluck(resolver.childNodes(this), 'loc') + } + }; + } } }); + +iD.Way.areaKeys = iD.util.trueObj(['area', 'building', 'leisure', 'tourism', 'ruins', + 'historic', 'landuse', 'military', 'natural', 'amenity', 'shop', 'man_made', + 'public_transport']); diff --git a/js/id/geo.js b/js/id/geo.js index fcce74b9a..682980758 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -9,16 +9,17 @@ iD.geo.interp = function(p1, p2, t) { p1[1] + (p2[1] - p1[1]) * t]; }; +// http://jsperf.com/id-dist-optimization iD.geo.dist = function(a, b) { - return Math.sqrt(Math.pow(a[0] - b[0], 2) + - Math.pow(a[1] - b[1], 2)); + var x = a[0] - b[0], y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); }; -iD.geo.chooseIndex = function(way, point, map) { +iD.geo.chooseIndex = function(way, point, context) { var dist = iD.geo.dist, - graph = map.history().graph(), + graph = context.graph(), nodes = graph.childNodes(way), - projNodes = nodes.map(function(n) { return map.projection(n.loc); }); + projNodes = nodes.map(function(n) { return context.projection(n.loc); }); for (var i = 0, changes = []; i < projNodes.length - 1; i++) { changes[i] = @@ -63,13 +64,13 @@ iD.geo.pointInPolygon = function(point, polygon) { }; iD.geo.polygonContainsPolygon = function(outer, inner) { - return _.every(inner, function (point) { + return _.every(inner, function(point) { return iD.geo.pointInPolygon(point, outer); }); }; iD.geo.polygonIntersectsPolygon = function(outer, inner) { - return _.some(inner, function (point) { + return _.some(inner, function(point) { return iD.geo.pointInPolygon(point, outer); }); }; diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js index 80bf631c6..243ebad4f 100644 --- a/js/id/geo/extent.js +++ b/js/id/geo/extent.js @@ -1,4 +1,4 @@ -iD.geo.Extent = function (min, max) { +iD.geo.Extent = function geoExtent(min, max) { if (!(this instanceof iD.geo.Extent)) return new iD.geo.Extent(min, max); if (min instanceof iD.geo.Extent) { return min; @@ -14,21 +14,21 @@ iD.geo.Extent = function (min, max) { iD.geo.Extent.prototype = [[], []]; _.extend(iD.geo.Extent.prototype, { - extend: function (obj) { - obj = iD.geo.Extent(obj); + extend: function(obj) { + if (!(obj instanceof iD.geo.Extent)) obj = new iD.geo.Extent(obj); return iD.geo.Extent([Math.min(obj[0][0], this[0][0]), Math.min(obj[0][1], this[0][1])], [Math.max(obj[1][0], this[1][0]), Math.max(obj[1][1], this[1][1])]); }, - center: function () { + center: function() { return [(this[0][0] + this[1][0]) / 2, (this[0][1] + this[1][1]) / 2]; }, - intersects: function (obj) { - obj = iD.geo.Extent(obj); + intersects: function(obj) { + if (!(obj instanceof iD.geo.Extent)) obj = new iD.geo.Extent(obj); return obj[0][0] <= this[1][0] && obj[0][1] <= this[1][1] && obj[1][0] >= this[0][0] && diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js deleted file mode 100644 index 120fa2fe7..000000000 --- a/js/id/graph/graph.js +++ /dev/null @@ -1,214 +0,0 @@ -iD.Graph = function(entities, mutable) { - if (!(this instanceof iD.Graph)) return new iD.Graph(entities, mutable); - - if (_.isArray(entities)) { - this.entities = {}; - for (var i = 0; i < entities.length; i++) { - this.entities[entities[i].id] = entities[i]; - } - } else { - this.entities = entities || {}; - } - - this.transients = {}; - this._parentWays = {}; - this._parentRels = {}; - this._childNodes = {}; - - if (!mutable) { - this.freeze(); - } -}; - -iD.Graph.prototype = { - entity: function(id) { - return this.entities[id]; - }, - - transient: function(entity, key, fn) { - var id = entity.id, - transients = this.transients[id] || - (this.transients[id] = {}); - - if (transients[key] !== undefined) { - return transients[key]; - } - - return transients[key] = fn.call(entity); - }, - - parentWays: function(entity) { - var ent, id, parents; - - if (!this._parentWays.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'way') { - for (var j = 0; j < ent.nodes.length; j++) { - id = ent.nodes[j]; - parents = this._parentWays[id] = this._parentWays[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentWays.calculated = true; - } - - return this._parentWays[entity.id] || []; - }, - - isPoi: function(entity) { - return this.parentWays(entity).length === 0; - }, - - parentRelations: function(entity) { - var ent, id, parents; - - if (!this._parentRels.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'relation') { - for (var j = 0; j < ent.members.length; j++) { - id = ent.members[j].id; - parents = this._parentRels[id] = this._parentRels[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentRels.calculated = true; - } - - return this._parentRels[entity.id] || []; - }, - - childNodes: function(entity) { - if (this._childNodes[entity.id]) - return this._childNodes[entity.id]; - - var nodes = []; - for (var i = 0, l = entity.nodes.length; i < l; i++) { - nodes[i] = this.entity(entity.nodes[i]); - } - - return (this._childNodes[entity.id] = nodes); - }, - - merge: function(graph) { - return this.update(function () { - _.defaults(this.entities, graph.entities); - }); - }, - - replace: function(entity) { - return this.update(function () { - this.entities[entity.id] = entity; - }); - }, - - remove: function(entity) { - return this.update(function () { - if (entity.created()) { - delete this.entities[entity.id]; - } else { - this.entities[entity.id] = undefined; - } - }); - }, - - update: function() { - var graph = this.frozen ? iD.Graph(_.clone(this.entities), true) : this; - - for (var i = 0; i < arguments.length; i++) { - arguments[i].call(graph, graph); - } - - return this.frozen ? graph.freeze() : this; - }, - - freeze: function() { - this.frozen = true; - - if (iD.debug) { - Object.freeze(this); - Object.freeze(this.entities); - } - - return this; - }, - - // get all objects that intersect an extent. - intersects: function(extent) { - var items = []; - for (var i in this.entities) { - var entity = this.entities[i]; - if (entity && entity.intersects(extent, this)) { - items.push(entity); - } - } - return items; - }, - - difference: function (graph) { - var result = [], entity, oldentity, id; - - for (id in this.entities) { - entity = this.entities[id]; - oldentity = graph.entities[id]; - if (entity !== oldentity) { - - if (entity && entity.type === 'way' && - oldentity && oldentity.type === 'way') { - result = result - .concat(_.difference(entity.nodes, oldentity.nodes)) - .concat(_.difference(oldentity.nodes, entity.nodes)); - - } else if (entity && entity.type === 'way') { - result = result.concat(entity.nodes); - - } else if (oldentity && oldentity.type === 'way') { - result = result.concat(oldentity.nodes); - } - - result.push(id); - } - } - - for (id in graph.entities) { - entity = graph.entities[id]; - if (entity && !this.entities.hasOwnProperty(id)) { - result.push(id); - if (entity.type === 'way') result = result.concat(entity.nodes); - } - } - - return result.sort(); - }, - - modified: function() { - var result = []; - _.each(this.entities, function(entity, id) { - if (entity && entity.modified()) result.push(id); - }); - return result; - }, - - created: function() { - var result = []; - _.each(this.entities, function(entity, id) { - if (entity && entity.created()) result.push(id); - }); - return result; - }, - - deleted: function() { - var result = []; - _.each(this.entities, function(entity, id) { - if (!entity) result.push(id); - }); - return result; - } -}; diff --git a/js/id/graph/history.js b/js/id/graph/history.js deleted file mode 100644 index 29b652554..000000000 --- a/js/id/graph/history.js +++ /dev/null @@ -1,158 +0,0 @@ -iD.History = function() { - var stack, index, - imagery_used = 'Bing', - dispatch = d3.dispatch('change', 'undone', 'redone'); - - function perform(actions) { - actions = Array.prototype.slice.call(actions); - - var annotation; - - if (!_.isFunction(_.last(actions))) { - annotation = actions.pop(); - } - - var graph = stack[index].graph; - for (var i = 0; i < actions.length; i++) { - graph = actions[i](graph); - } - - return {graph: graph, annotation: annotation, imagery_used: imagery_used}; - } - - function change(previous) { - dispatch.change(history.graph().difference(previous)); - } - - var history = { - graph: function () { - return stack[index].graph; - }, - - merge: function (graph) { - for (var i = 0; i < stack.length; i++) { - stack[i].graph = stack[i].graph.merge(graph); - } - }, - - perform: function () { - var previous = stack[index].graph; - - stack = stack.slice(0, index + 1); - stack.push(perform(arguments)); - index++; - - change(previous); - }, - - replace: function () { - var previous = stack[index].graph; - - // assert(index == stack.length - 1) - stack[index] = perform(arguments); - - change(previous); - }, - - pop: function () { - var previous = stack[index].graph; - - if (index > 0) { - index--; - stack.pop(); - change(previous); - } - }, - - undo: function () { - var previous = stack[index].graph; - - // Pop to the first annotated state. - while (index > 0) { - if (stack[index].annotation) break; - index--; - } - - // Pop to the next annotated state. - while (index > 0) { - index--; - if (stack[index].annotation) break; - } - - dispatch.undone(); - change(previous); - }, - - redo: function () { - var previous = stack[index].graph; - - while (index < stack.length - 1) { - index++; - if (stack[index].annotation) break; - } - - dispatch.redone(); - change(previous); - }, - - undoAnnotation: function () { - var i = index; - while (i >= 0) { - if (stack[i].annotation) return stack[i].annotation; - i--; - } - }, - - redoAnnotation: function () { - var i = index + 1; - while (i <= stack.length - 1) { - if (stack[i].annotation) return stack[i].annotation; - i++; - } - }, - - changes: function () { - var initial = stack[0].graph, - current = stack[index].graph; - - return { - modified: current.modified().map(function (id) { - return current.entity(id); - }), - created: current.created().map(function (id) { - return current.entity(id); - }), - deleted: current.deleted().map(function (id) { - return initial.entity(id); - }) - }; - }, - - hasChanges: function() { - return !!this.numChanges(); - }, - - numChanges: function() { - return d3.sum(d3.values(this.changes()).map(function(c) { - return c.length; - })); - }, - - imagery_used: function(source) { - if (source) imagery_used = source; - else return _.without( - _.unique(_.pluck(stack.slice(1, index + 1), 'imagery_used')), - undefined, 'Custom'); - }, - - reset: function () { - stack = [{graph: iD.Graph()}]; - index = 0; - dispatch.change(); - } - }; - - history.reset(); - - return d3.rebind(history, dispatch, 'on'); -}; diff --git a/js/id/graph/validate.js b/js/id/graph/validate.js deleted file mode 100644 index 805b775c3..000000000 --- a/js/id/graph/validate.js +++ /dev/null @@ -1,47 +0,0 @@ -iD.validate = function(changes, graph) { - var warnings = [], change; - - // https://github.com/openstreetmap/josm/blob/mirror/src/org/ - // openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80 - function tagSuggestsArea(change) { - if (_.isEmpty(change.tags)) return false; - var tags = change.tags; - var presence = ['landuse', 'amenities', 'tourism', 'shop']; - for (var i = 0; i < presence.length; i++) { - if (tags[presence[i]] !== undefined) { - return presence[i] + '=' + tags[presence[i]]; - } - } - if (tags.building && tags.building === 'yes') return 'building=yes'; - } - - if (changes.created.length) { - for (var i = 0; i < changes.created.length; i++) { - change = changes.created[i]; - - if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) { - warnings.push({ - message: 'Untagged point which is not part of a line or area', - entity: change - }); - } - - if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) { - warnings.push({ message: 'Untagged line', entity: change }); - } - - if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) { - warnings.push({ message: 'Untagged area', entity: change }); - } - - if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) { - warnings.push({ - message: 'The tag ' + tagSuggestsArea(change) + ' suggests line should be area, but it is not and area', - entity: change - }); - } - } - } - - return warnings.length ? [warnings] : []; -}; diff --git a/js/id/id.js b/js/id/id.js index ac700f846..5b4b1fb73 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -1,288 +1,149 @@ -window.iD = function(container) { - // the reported, displayed version of iD. - var version = '0.0.0-alpha1'; +window.iD = function () { + var context = {}, + storage = localStorage || {}; - var connection = iD.Connection() - .version(version), - history = iD.History(), - map = iD.Map() - .connection(connection) - .history(history), - controller = iD.Controller(map, history); + context.storage = function(k, v) { + if (arguments.length === 1) return storage[k]; + else if (v === null) delete storage[k]; + else storage[k] = v; + }; - map.background.source(iD.BackgroundSource.Bing); + var history = iD.History(context), + dispatch = d3.dispatch('enter', 'exit'), + mode, + container, + ui = iD.ui(context), + map = iD.Map(context); - function editor(container) { - if (!iD.supported()) { - container.html('This editor is supported in Firefox, Chrome, Safari, Opera, ' + - 'and Internet Explorer 9 and above. Please upgrade your browser ' + - 'or use Potlatch 2 to edit the map.') - .style('text-align:center;font-style:italic;'); - return; + // the connection requires .storage() to be available on calling. + var connection = iD.Connection(context); + + connection.on('load.context', function loadContext(err, result) { + history.merge(result); + }); + + /* Straight accessors. Avoid using these if you can. */ + context.ui = function() { return ui; }; + context.connection = function() { return connection; }; + context.history = function() { return history; }; + context.map = function() { return map; }; + + /* History */ + context.graph = history.graph; + context.perform = history.perform; + context.replace = history.replace; + context.pop = history.pop; + context.undo = history.undo; + context.redo = history.redo; + context.changes = history.changes; + + /* Graph */ + context.entity = function(id) { + return history.graph().entity(id); + }; + + context.geometry = function(id) { + return context.entity(id).geometry(history.graph()); + }; + + /* Modes */ + context.enter = function(newMode) { + if (mode) { + mode.exit(); + dispatch.exit(mode); } - function hintprefix(x, y) { - return '' + x + ' ' + y; + mode = newMode; + mode.enter(); + dispatch.enter(mode); + }; + + context.mode = function() { + return mode; + }; + + context.selection = function() { + if (mode.id === 'select') { + return mode.selection(); + } else { + return []; } + }; - var m = container.append('div') - .attr('id', 'map') - .call(map); + /* Behaviors */ + context.install = function(behavior) { + context.surface().call(behavior); + }; - var bar = container.append('div') - .attr('id', 'bar') - .attr('class','pad1 fillD'); + context.uninstall = function(behavior) { + context.surface().call(behavior.off); + }; - var limiter = bar.append('div') - .attr('class', 'limiter'); + /* Map */ + context.background = function() { return map.background; }; + context.surface = function() { return map.surface; }; + context.projection = map.projection; + context.tail = map.tail; + context.redraw = map.redraw; + context.pan = map.pan; + context.zoomIn = map.zoomIn; + context.zoomOut = map.zoomOut; - var buttons_joined = limiter.append('div') - .attr('class', 'button-wrap joined col4'); + context.container = function(_) { + if (!arguments.length) return container; + container = _; + return context; + }; - var buttons = buttons_joined.selectAll('button.add-button') - .data([iD.modes.Browse(), iD.modes.AddPoint(), iD.modes.AddLine(), iD.modes.AddArea()]) - .enter().append('button') - .attr('tabindex', -1) - .attr('class', function (mode) { return mode.title + ' add-button col3'; }) - .attr('data-original-title', function (mode) { - return hintprefix(mode.key, mode.description); - }) - .call(bootstrap.tooltip().placement('bottom').html(true)) - .on('click.editor', function (mode) { controller.enter(mode); }); - - function disableTooHigh() { - if (map.editable()) { - notice.message(false); - buttons.attr('disabled', null); - } else { - buttons.attr('disabled', 'disabled'); - notice.message(true); - controller.enter(iD.modes.Browse()); - } - } - - var notice = iD.ui.notice(limiter) - .message(false) - .on('zoom', function() { map.zoom(16); }); - - map.on('move.editor', _.debounce(function() { - disableTooHigh(); - contributors.call(iD.ui.contributors(map)); - }, 500)); - - buttons.append('span') - .attr('class', function(d) { - return d.id + ' icon icon-pre-text'; - }); - - buttons.append('span').attr('class', 'label').text(function (mode) { return mode.title; }); - - controller.on('enter.editor', function (entered) { - buttons.classed('active', function (mode) { return entered.button === mode.button; }); - container.classed("mode-" + entered.id, true); - }); - - controller.on('exit.editor', function (exited) { - container.classed("mode-" + exited.id, false); - }); - - var undo_buttons = limiter.append('div') - .attr('class', 'button-wrap joined col1'), - undo_tooltip = bootstrap.tooltip().placement('bottom').html(true); - - undo_buttons.append('button') - .attr({ id: 'undo', 'class': 'col6' }) - .property('disabled', true) - .html("") - .on('click.editor', history.undo) - .call(undo_tooltip); - - undo_buttons.append('button') - .attr({ id: 'redo', 'class': 'col6' }) - .property('disabled', true) - .html("") - .on('click.editor', history.redo) - .call(undo_tooltip); - - var save_button = limiter.append('div').attr('class','button-wrap col1').append('button') - .attr('class', 'save col12') - .call(iD.ui.save().map(map).controller(controller)); - - var zoom = container.append('div') - .attr('class', 'zoombuttons map-control') - .selectAll('button') - .data([['zoom-in', '+', map.zoomIn, 'Zoom In'], ['zoom-out', '-', map.zoomOut, 'Zoom Out']]) - .enter() - .append('button') - .attr('tabindex', -1) - .attr('class', function(d) { return d[0]; }) - .attr('title', function(d) { return d[3]; }) - .on('click.editor', function(d) { return d[2](); }) - .append('span') - .attr('class', function(d) { - return d[0] + ' icon'; - }); - - if (navigator.geolocation) { - container.append('div') - .call(iD.ui.geolocate(map)); - } - - var gc = container.append('div').attr('class', 'geocode-control map-control') - .call(iD.ui.geocoder().map(map)); - - container.append('div').attr('class', 'map-control layerswitcher-control') - .call(iD.ui.layerswitcher(map)); - - container.append('div') - .style('display', 'none') - .attr('class', 'inspector-wrap fr col5'); - - var about = container.append('div') - .attr('class','col12 about-block fillD pad1'); - - about.append('div') - .attr('class', 'user-container') - .append('div') - .attr('class', 'hello'); - - var aboutList = about.append('ul') - .attr('id','about') - .attr('class','link-list'); - - var linkList = aboutList.append('ul') - .attr('id','about') - .attr('class','pad1 fillD about-block link-list'); - linkList.append('li').append('a').attr('target', '_blank') - .attr('href', 'http://github.com/systemed/iD').text(version); - linkList.append('li').append('a').attr('target', '_blank') - .attr('href', 'http://github.com/systemed/iD/issues').text('report a bug'); - - var imagery = linkList.append('li').attr('id', 'attribution'); - imagery.append('span').text('imagery'); - imagery.append('a').attr('target', '_blank') - .attr('href', 'http://opengeodata.org/microsoft-imagery-details').text(' provided by bing'); - - linkList.append('li').attr('class', 'source-switch').append('a').attr('href', '#') - .text('dev') - .on('click.editor', function() { - d3.event.preventDefault(); - if (d3.select(this).classed('live')) { - map.flush().connection() - .url('http://api06.dev.openstreetmap.org'); - d3.select(this).text('dev').classed('live', false); - } else { - map.flush().connection() - .url('http://www.openstreetmap.org'); - d3.select(this).text('live').classed('live', true); - } - }); - - var contributors = linkList.append('li') - .attr('id', 'user-list'); - contributors.append('span') - .attr('class', 'icon nearby icon-pre-text'); - contributors.append('span') - .text('Viewing contributions by '); - contributors.append('span') - .attr('class', 'contributor-list'); - contributors.append('span') - .attr('class', 'contributor-count'); - - history.on('change.editor', function() { - window.onbeforeunload = history.hasChanges() ? function() { - return 'You have unsaved changes.'; - } : null; - - var undo = history.undoAnnotation(), - redo = history.redoAnnotation(); - - function refreshTooltip(selection) { - if (selection.property('disabled')) { - selection.call(undo_tooltip.hide); - } else if (selection.property('tooltipVisible')) { - selection.call(undo_tooltip.show); - } - } - - limiter.select('#undo') - .property('disabled', !undo) - .attr('data-original-title', hintprefix('⌘Z', undo)) - .call(refreshTooltip); - - limiter.select('#redo') - .property('disabled', !redo) - .attr('data-original-title', hintprefix('⌘⇧Z', redo)) - .call(refreshTooltip); - }); - - d3.select(window).on('resize.editor', function() { - map.size(m.size()); - }); - - var keybinding = d3.keybinding('main') - .on('⌘+Z', function() { history.undo(); }) - .on('⌃+Z', function() { history.undo(); }) - .on('⌘+⇧+Z', function() { history.redo(); }) - .on('⌃+⇧+Z', function() { history.redo(); }) - .on('⌫', function() { d3.event.preventDefault(); }); - - [iD.modes.Browse(), iD.modes.AddPoint(), iD.modes.AddLine(), iD.modes.AddArea()].forEach(function(m) { - keybinding.on(m.key, function() { if (map.editable()) controller.enter(m); }); - }); - - d3.select(document) - .call(keybinding); - - var hash = iD.Hash().controller(controller).map(map); - - if (!hash.hadHash) { - map.centerZoom([-77.02271, 38.90085], 20); - } - - d3.select('.user-container').call(iD.ui.userpanel(connection) - .on('logout.editor', connection.logout) - .on('login.editor', connection.authenticate)); - - controller.enter(iD.modes.Browse()); - - if (!localStorage.sawSplash) { - iD.ui.splash(); - localStorage.sawSplash = true; - } + var q = iD.util.stringQs(location.hash.substring(1)), detected = false; + if (q.layer) { + context.background() + .source(_.find(iD.layers, function(l) { + if (l.data.sourcetag === q.layer) { + return (detected = true); + } + })); } - editor.connection = function(_) { - if (!arguments.length) return connection; - connection = _; - return editor; - }; - - editor.map = function() { - return map; - }; - - editor.controller = function() { - return controller; - }; - - if (arguments.length) { - d3.select(container).call(editor); + if (!detected) { + context.background() + .source(_.find(iD.layers, function(l) { + return l.data.name === 'Bing aerial imagery'; + })); } - return editor; + return d3.rebind(context, dispatch, 'on'); }; -iD.supported = function() { - if (navigator.appName !== 'Microsoft Internet Explorer') { - return true; +iD.version = '0.0.0-alpha1'; + +iD.detect = function() { + var browser = {}; + + var ua = navigator.userAgent, + msie = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})"); + + if (msie.exec(ua) !== null) { + var rv = parseFloat(RegExp.$1); + browser.support = !(rv && rv < 9); } else { - var ua = navigator.userAgent; - var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})"); - if (re.exec(ua) !== null) { - rv = parseFloat( RegExp.$1 ); - } - if (rv && rv < 9) return false; - else return true; + browser.support = true; } + + // Added due to incomplete svg style support. See #715 + browser.opera = ua.indexOf('Opera') >= 0; + + browser.locale = navigator.language; + + function nav(x) { + return navigator.userAgent.indexOf(x) !== -1; + } + + if (nav('Win')) browser.os = 'win'; + else if (nav('Mac')) browser.os = 'mac'; + else if (nav('X11')) browser.os = 'linux'; + else if (nav('Linux')) browser.os = 'linux'; + else browser.os = 'win'; + + return browser; }; diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index d1a3efb08..1614a3997 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -1,72 +1,66 @@ -iD.modes.AddArea = function() { +iD.modes.AddArea = function(context) { var mode = { id: 'add-area', button: 'area', - title: 'Area', - description: 'Add parks, buildings, lakes, or other areas to the map.', - key: 'a' + title: t('modes.add_area.title'), + description: t('modes.add_area.description'), + key: '4' }; - var behavior, + var behavior = iD.behavior.AddWay(context) + .on('start', start) + .on('startFromWay', startFromWay) + .on('startFromNode', startFromNode), defaultTags = {area: 'yes'}; + function start(loc) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromWay(other, loc, index) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(other.id, node.id, index)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + + function startFromNode(node) { + var graph = context.graph(), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawArea(context, way.id, graph)); + } + mode.enter = function() { - var map = mode.map, - history = mode.history, - controller = mode.controller; - - function startFromNode(node) { - var graph = history.graph(), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(way.id, node.id)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - function startFromWay(other, loc, index) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(other.id, node.id, index)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - function start(loc) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(way.id, node.id)); - - controller.enter(iD.modes.DrawArea(way.id, graph)); - } - - behavior = iD.behavior.AddWay(mode) - .on('startFromNode', startFromNode) - .on('startFromWay', startFromWay) - .on('start', start); - - mode.map.surface.call(behavior); - mode.map.tail('Click on the map to start drawing an area, like a park, lake, or building.', true); + context.install(behavior); + context.tail(t('modes.add_area.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index bf419d2bb..ae1d03cef 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -1,80 +1,74 @@ -iD.modes.AddLine = function() { +iD.modes.AddLine = function(context) { var mode = { id: 'add-line', button: 'line', - title: 'Line', - description: 'Lines can be highways, streets, pedestrian paths, or even canals.', - key: 'l' + title: t('modes.add_line.title'), + description: t('modes.add_line.description'), + key: '3' }; - var behavior, + var behavior = iD.behavior.AddWay(context) + .on('start', start) + .on('startFromWay', startFromWay) + .on('startFromNode', startFromNode), defaultTags = {highway: 'residential'}; + function start(loc) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + + function startFromWay(other, loc, index) { + var graph = context.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(node), + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id), + iD.actions.AddVertex(other.id, node.id, index)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + + function startFromNode(node) { + var graph = context.graph(), + parent = graph.parentWays(node)[0], + isLine = parent && parent.geometry(graph) === 'line'; + + if (isLine && parent.first() === node.id) { + context.enter(iD.modes.DrawLine(context, parent.id, 'backward', graph)); + + } else if (isLine && parent.last() === node.id) { + context.enter(iD.modes.DrawLine(context, parent.id, 'forward', graph)); + + } else { + var way = iD.Way({tags: defaultTags}); + + context.perform( + iD.actions.AddEntity(way), + iD.actions.AddVertex(way.id, node.id)); + + context.enter(iD.modes.DrawLine(context, way.id, 'forward', graph)); + } + } + mode.enter = function() { - var map = mode.map, - history = mode.history, - controller = mode.controller; - - function startFromNode(node) { - var graph = history.graph(), - parent = graph.parentWays(node)[0], - isLine = parent && parent.geometry(graph) === 'line'; - - if (isLine && parent.first() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'backward', graph)); - - } else if (isLine && parent.last() === node.id) { - controller.enter(iD.modes.DrawLine(parent.id, 'forward', graph)); - - } else { - var way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - } - - function startFromWay(other, loc, index) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(other.id, node.id, index)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - function start(loc) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - behavior = iD.behavior.AddWay(mode) - .on('startFromNode', startFromNode) - .on('startFromWay', startFromWay) - .on('start', start); - - mode.map.surface.call(behavior); - mode.map.tail('Click on the map to start drawing an road, path, or route.', true); + context.install(behavior); + context.tail(t('modes.add_line.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 1a51c314d..47bdafae8 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -1,48 +1,48 @@ -iD.modes.AddPoint = function() { +iD.modes.AddPoint = function(context) { var mode = { id: 'add-point', - title: 'Point', - description: 'Restaurants, monuments, and postal boxes are points.', - key: 'p' + title: t('modes.add_point.title'), + description: t('modes.add_point.description'), + key: '2' }; - var behavior; + var behavior = iD.behavior.Draw(context) + .on('click', add) + .on('clickWay', addWay) + .on('clickNode', addNode) + .on('cancel', cancel) + .on('finish', cancel); + + function add(loc) { + var node = iD.Node({loc: loc}); + + context.perform( + iD.actions.AddEntity(node), + t('operations.add.annotation.point')); + + context.enter(iD.modes.Select(context, [node.id], true)); + } + + function addWay(way, loc, index) { + add(loc); + } + + function addNode(node) { + add(node.loc); + } + + function cancel() { + context.enter(iD.modes.Browse(context)); + } mode.enter = function() { - var map = mode.map, - surface = map.surface, - history = mode.history, - controller = mode.controller; - - map.tail('Click on the map to add a point.', true); - - function add() { - var node = iD.Node({loc: map.mouseCoordinates()}); - - history.perform( - iD.actions.AddNode(node), - 'added a point'); - - controller.enter(iD.modes.Select(node, true)); - } - - function cancel() { - controller.exit(); - } - - behavior = iD.behavior.Draw() - .on('add', add) - .on('cancel', cancel) - .on('finish', cancel) - (surface); + context.install(behavior); + context.tail(t('modes.add_point.tail')); }; mode.exit = function() { - var map = mode.map, - surface = map.surface; - - map.tail(false); - behavior.off(surface); + context.uninstall(behavior); + context.tail(false); }; return mode; diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 9782bac27..d26ea4a55 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -1,42 +1,28 @@ -iD.modes.Browse = function() { +iD.modes.Browse = function(context) { var mode = { button: 'browse', id: 'browse', - title: 'Browse', - description: 'Pan and zoom the map', - key: 'b' + title: t('modes.browse.title'), + description: t('modes.browse.description'), + key: '1' }; - var behaviors; + var behaviors = [ + iD.behavior.Hover(), + iD.behavior.Select(context), + iD.behavior.Lasso(context), + iD.behavior.DragNode(context)]; mode.enter = function() { - var surface = mode.map.surface; - - behaviors = [ - iD.behavior.Hover(), - iD.behavior.DragNode(mode), - iD.behavior.DragMidpoint(mode)]; - behaviors.forEach(function(behavior) { - behavior(surface); - }); - - surface.on('click.browse', function () { - var datum = d3.select(d3.event.target).datum(); - if (datum instanceof iD.Entity) { - mode.controller.enter(iD.modes.Select(datum)); - } + context.install(behavior); }); }; mode.exit = function() { - var surface = mode.map.surface; - behaviors.forEach(function(behavior) { - behavior.off(surface); + context.uninstall(behavior); }); - - surface.on('click.browse', null); }; return mode; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 721452bae..aee2f153c 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -1,4 +1,4 @@ -iD.modes.DrawArea = function(wayId, baseGraph) { +iD.modes.DrawArea = function(context, wayId, baseGraph) { var mode = { button: 'area', id: 'draw-area' @@ -7,41 +7,28 @@ iD.modes.DrawArea = function(wayId, baseGraph) { var behavior; mode.enter = function() { - var way = mode.history.graph().entity(wayId), - index = -1, + var way = context.entity(wayId), headId = way.nodes[way.nodes.length - 2], - tailId = way.first(), - annotation = way.isDegenerate() ? 'started an area' : 'continued an area'; + tailId = way.first(); - function addHeadTail() { - behavior.finish(); - } + behavior = iD.behavior.DrawWay(context, wayId, -1, mode, baseGraph); - function addNode(node) { - behavior.addNode(node, annotation); - } + var addNode = behavior.addNode; - function addWay(way, loc, index) { - behavior.addWay(way, loc, index, annotation); - } + behavior.addNode = function(node) { + if (node.id === headId || node.id === tailId) { + behavior.finish(); + } else { + addNode(node); + } + }; - function add(loc) { - behavior.add(loc, annotation); - } - - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) - .on('addHead', addHeadTail) - .on('addTail', addHeadTail) - .on('addNode', addNode) - .on('addWay', addWay) - .on('add', add); - - mode.map.surface.call(behavior); - mode.map.tail('Click to add points to your area. Click the first point to finish the area.', true); + context.install(behavior); + context.tail(t('modes.draw_area.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 289d91c14..9fd07c937 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -1,4 +1,4 @@ -iD.modes.DrawLine = function(wayId, direction, baseGraph) { +iD.modes.DrawLine = function(context, wayId, direction, baseGraph) { var mode = { button: 'line', id: 'draw-line' @@ -7,52 +7,28 @@ iD.modes.DrawLine = function(wayId, direction, baseGraph) { var behavior; mode.enter = function() { - var way = mode.history.graph().entity(wayId), + var way = context.entity(wayId), index = (direction === 'forward') ? undefined : 0, - headId = (direction === 'forward') ? way.last() : way.first(), - tailId = (direction === 'forward') ? way.first() : way.last(), - annotation = way.isDegenerate() ? 'started a line' : 'continued a line'; + headId = (direction === 'forward') ? way.last() : way.first(); - function addHead() { - behavior.finish(); - } + behavior = iD.behavior.DrawWay(context, wayId, index, mode, baseGraph); - function addTail(node) { - // connect the way in a loop - if (way.nodes.length > 2) { - behavior.addNode(node, annotation); + var addNode = behavior.addNode; + + behavior.addNode = function(node) { + if (node.id === headId) { + behavior.finish(); } else { - behavior.cancel(); + addNode(node); } - } + }; - function addNode(node) { - behavior.addNode(node, annotation); - } - - function addWay(way, loc, index) { - behavior.addWay(way, loc, index, annotation); - } - - function add(loc) { - behavior.add(loc, annotation); - } - - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) - .on('addHead', addHead) - .on('addTail', addTail) - .on('addNode', addNode) - .on('addWay', addWay) - .on('add', add); - - mode.map.surface.call(behavior); - mode.map.tail('Click to add more points to the line. ' + - 'Click on other lines to connect to them, and double-click to ' + - 'end the line.', true); + context.install(behavior); + context.tail(t('modes.draw_line.tail')); }; mode.exit = function() { - mode.map.surface.call(behavior.off); + context.uninstall(behavior); }; return mode; diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js index 7fe07d1ce..cdd9fa792 100644 --- a/js/id/modes/move_way.js +++ b/js/id/modes/move_way.js @@ -1,51 +1,88 @@ -iD.modes.MoveWay = function(wayId) { +iD.modes.MoveWay = function(context, wayId) { var mode = { - id: 'move-way' + id: 'move-way', + button: 'browse' }; var keybinding = d3.keybinding('move-way'); mode.enter = function() { - var map = mode.map, - history = mode.history, - graph = history.graph(), - selection = map.surface, - controller = mode.controller, - projection = map.projection; + var origin = context.map().mouseCoordinates(), + nudgeInterval, + annotation = t('operations.move.annotation.' + context.geometry(wayId)); - var way = graph.entity(wayId), - origin = d3.mouse(selection.node()); + // If intiated via keyboard + if (!origin[0] && !origin[1]) origin = null; - history.perform( + context.perform( iD.actions.Noop(), - 'moved a way'); + annotation); + + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.pan(nudge); + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + + function point() { + return d3.mouse(context.map().surface.node()); + } function move() { - var p = d3.mouse(selection.node()), - delta = [p[0] - origin[0], - p[1] - origin[1]]; + var p = point(); - origin = p; + var delta = origin ? + [p[0] - context.projection(origin)[0], + p[1] - context.projection(origin)[1]] : + [0, 0]; - history.replace( - iD.actions.MoveWay(wayId, delta, projection), - 'moved a way'); + var nudge = edge(p, context.map().size()); + if (nudge) startNudge(nudge); + else stopNudge(); + + origin = context.map().mouseCoordinates(); + + context.replace( + iD.actions.MoveWay(wayId, delta, context.projection), + annotation); } function finish() { d3.event.stopPropagation(); - controller.enter(iD.modes.Select(way, true)); + context.enter(iD.modes.Select(context, [wayId], true)); } function cancel() { - history.pop(); - controller.enter(iD.modes.Select(way, true)); + context.pop(); + context.enter(iD.modes.Select(context, [wayId], true)); } - selection + function undone() { + context.enter(iD.modes.Browse(context)); + } + + context.surface() .on('mousemove.move-way', move) .on('click.move-way', finish); + context.history() + .on('undone.move-way', undone); + keybinding .on('⎋', cancel) .on('↩', finish); @@ -55,13 +92,13 @@ iD.modes.MoveWay = function(wayId) { }; mode.exit = function() { - var map = mode.map, - selection = map.surface; - - selection + context.surface() .on('mousemove.move-way', null) .on('click.move-way', null); + context.history() + .on('undone.move-way', null); + keybinding.off(); }; diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 06c05b7ea..9998852df 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -1,164 +1,217 @@ -iD.modes.Select = function(entity, initial) { +iD.modes.Select = function(context, selection, initial) { var mode = { id: 'select', - button: 'browse', - entity: entity + button: 'browse' }; - var inspector = iD.ui.inspector().initial(!!initial), + var inspector = iD.ui.Inspector().initial(!!initial), keybinding = d3.keybinding('select'), - behaviors, - radialMenu; - - function remove() { - if (entity.type === 'way') { - mode.history.perform( - iD.actions.DeleteWay(entity.id), - 'deleted a way'); - } else if (entity.type === 'node') { - mode.history.perform( - iD.actions.DeleteNode(entity.id), - 'deleted a node'); - } - - mode.controller.exit(); - } - - function changeTags(d, tags) { - if (!_.isEqual(entity.tags, tags)) { - mode.history.perform( - iD.actions.ChangeEntityTags(d.id, tags), - 'changed tags'); - } - } - - mode.enter = function() { - var map = mode.map, - graph = map.history().graph(), - history = map.history(), - surface = mode.map.surface; - - inspector.graph(graph); - + timeout = null, behaviors = [ iD.behavior.Hover(), - iD.behavior.DragNode(mode), - iD.behavior.DragMidpoint(mode)]; + iD.behavior.Select(context), + iD.behavior.Lasso(context), + iD.behavior.DragNode(context)], + radialMenu; + + function changeTags(d, tags) { + if (!_.isEqual(singular().tags, tags)) { + context.perform( + iD.actions.ChangeTags(d.id, tags), + t('operations.change_tags.annotation')); + } + } + + function singular() { + if (selection.length === 1) { + return context.entity(selection[0]); + } + } + + function positionMenu() { + var entity = singular(); + + if (entity && entity.type === 'node') { + radialMenu.center(context.projection(entity.loc)); + } else { + radialMenu.center(d3.mouse(context.surface().node())); + } + } + + function showMenu() { + context.surface() + .call(radialMenu.close) + .call(radialMenu); + } + + mode.selection = function() { + return selection; + }; + + mode.reselect = function() { + positionMenu(); + showMenu(); + }; + + mode.enter = function() { + var entity = singular(); behaviors.forEach(function(behavior) { - behavior(surface); + context.install(behavior); + }); + + var operations = _.without(d3.values(iD.operations), iD.operations.Delete) + .map(function(o) { return o(selection, context); }) + .filter(function(o) { return o.available(); }); + operations.unshift(iD.operations.Delete(selection, context)); + + keybinding.on('⎋', function() { + context.enter(iD.modes.Browse(context)); + }); + + operations.forEach(function(operation) { + keybinding.on(operation.key, function() { + if (operation.enabled()) { + operation(); + } + }); }); var q = iD.util.stringQs(location.hash.substring(1)); location.replace('#' + iD.util.qsString(_.assign(q, { - id: entity.id + id: selection.join(',') }), true)); - d3.select('.inspector-wrap') - .style('display', 'block') - .style('opacity', 1) - .datum(entity) - .call(inspector); + if (entity) { + inspector.context(context); - if (d3.event) { - // Pan the map if the clicked feature intersects with the position - // of the inspector - var inspector_size = d3.select('.inspector-wrap').size(), - map_size = mode.map.size(), - offset = 50, - shift_left = d3.event.x - map_size[0] + inspector_size[0] + offset, - center = (map_size[0] / 2) + shift_left + offset; + context.container() + .select('.inspector-wrap') + .style('display', 'block') + .style('opacity', 1) + .datum(entity) + .call(inspector); - if (shift_left > 0 && inspector_size[1] > d3.event.y) { - mode.map.centerEase(mode.map.projection.invert([center, map_size[1]/2])); + if (d3.event) { + // Pan the map if the clicked feature intersects with the position + // of the inspector + var inspector_size = context.container().select('.inspector-wrap').size(), + map_size = context.map().size(), + offset = 50, + shift_left = d3.event.clientX - map_size[0] + inspector_size[0] + offset, + center = (map_size[0] / 2) + shift_left + offset; + + if (shift_left > 0 && inspector_size[1] > d3.event.clientY) { + context.map().centerEase(context.projection.invert([center, map_size[1]/2])); + } } + + inspector + .on('changeTags', changeTags) + .on('close', function() { context.enter(iD.modes.Browse(context)); }); } - inspector - .on('changeTags', changeTags) - .on('close', function() { mode.controller.exit(); }); + context.history().on('change.select', function() { + context.surface().call(radialMenu.close); - history.on('change.select', function() { - // Exit mode if selected entity gets undone - var old = entity; - entity = history.graph().entity(entity.id); - if (!entity) { - mode.controller.enter(iD.modes.Browse()); - } else if(!_.isEqual(entity.tags, old.tags)) { - inspector.tags(entity.tags); + if (_.any(selection, function(id) { return !context.entity(id); })) { + // Exit mode if selected entity gets undone + context.enter(iD.modes.Browse(context)); + + } else if (entity) { + var newEntity = context.entity(selection[0]); + if (!_.isEqual(entity.tags, newEntity.tags)) { + inspector.tags(newEntity.tags); + } } - - surface.call(radialMenu.close); }); - map.on('move.select', function() { - surface.call(radialMenu.close); + context.map().on('move.select', function() { + context.surface().call(radialMenu.close); }); - function click() { - var datum = d3.select(d3.event.target).datum(); - if (datum instanceof iD.Entity) { - mode.controller.enter(iD.modes.Select(datum)); - } else { - mode.controller.enter(iD.modes.Browse()); - } - } - function dblclick() { - var selection = d3.select(d3.event.target), - datum = selection.datum(); + var target = d3.select(d3.event.target), + datum = target.datum(); - if (datum instanceof iD.Way && !selection.classed('fill')) { + if (datum instanceof iD.Way && !target.classed('fill')) { var choice = iD.geo.chooseIndex(datum, - d3.mouse(mode.map.surface.node()), mode.map), + d3.mouse(context.surface().node()), context), node = iD.Node({ loc: choice.loc }); - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWayNode(datum.id, node.id, choice.index), - 'added a point to a road'); + var prev = datum.nodes[choice.index - 1], + next = datum.nodes[choice.index], + prevParents = context.graph().parentWays({ id: prev }), + ways = []; + + + for (var i = 0; i < prevParents.length; i++) { + var p = prevParents[i]; + for (var k = 0; k < p.nodes.length; k++) { + if (p.nodes[k] === prev) { + if (p.nodes[k-1] === next) { + ways.push({ id: p.id, index: k}); + break; + } else if (p.nodes[k+1] === next) { + ways.push({ id: p.id, index: k+1}); + break; + } + } + } + } + + context.perform(iD.actions.AddEntity(node), + iD.actions.AddMidpoint({ ways: ways, loc: node.loc }, node), + t('operations.add.annotation.vertex')); d3.event.preventDefault(); d3.event.stopPropagation(); } } - surface.on('click.select', click) - .on('dblclick.select', dblclick); - - keybinding.on('⌫', remove); + function selected(entity) { + if (!entity) return false; + if (selection.indexOf(entity.id) >= 0) return true; + return d3.select(this).classed('stroke') && + _.any(context.graph().parentRelations(entity), function(parent) { + return selection.indexOf(parent.id) >= 0; + }); + } d3.select(document) .call(keybinding); - surface.selectAll("*") - .filter(function (d) { - return d && entity && d.id === entity.id; - }) + context.surface() + .selectAll("*") + .filter(selected) .classed('selected', true); - radialMenu = iD.ui.RadialMenu(entity, mode); + radialMenu = iD.ui.RadialMenu(operations); + var show = d3.event && !initial; - if (d3.event && !initial) { - var loc = map.mouseCoordinates(); + if (show) { + positionMenu(); + } - if (entity.type === 'node') { - loc = entity.loc; + timeout = window.setTimeout(function() { + if (show) { + showMenu(); } - surface.call(radialMenu, map.projection(loc)); - } + context.surface() + .on('dblclick.select', dblclick); + }, 200); }; - mode.exit = function () { - var surface = mode.map.surface, - history = mode.history; - - if (entity) { - changeTags(entity, inspector.tags()); + mode.exit = function() { + if (singular()) { + changeTags(singular(), inspector.tags()); } - d3.select('.inspector-wrap') + if (timeout) window.clearTimeout(timeout); + + context.container() + .select('.inspector-wrap') .style('display', 'none') .html(''); @@ -167,7 +220,7 @@ iD.modes.Select = function(entity, initial) { d3.selectAll('div.typeahead').remove(); behaviors.forEach(function(behavior) { - behavior.off(surface); + context.uninstall(behavior); }); var q = iD.util.stringQs(location.hash.substring(1)); @@ -175,15 +228,14 @@ iD.modes.Select = function(entity, initial) { keybinding.off(); - surface.on('click.select', null) - .on('dblclick.select', null); + context.history() + .on('change.select', null); - history.on('change.select', null); - - surface.selectAll(".selected") + context.surface() + .call(radialMenu.close) + .on('dblclick.select', null) + .selectAll(".selected") .classed('selected', false); - - surface.call(radialMenu.close); }; return mode; diff --git a/js/id/oauth.js b/js/id/oauth.js index a09f2affc..ec6a12668 100644 --- a/js/id/oauth.js +++ b/js/id/oauth.js @@ -1,4 +1,4 @@ -iD.OAuth = function() { +iD.OAuth = function(context) { var baseurl = 'http://www.openstreetmap.org', o = {}, keys, @@ -6,10 +6,6 @@ iD.OAuth = function() { function keyclean(x) { return x.replace(/\W/g, ''); } - if (token('oauth_token')) { - o.oauth_token = token('oauth_token'); - } - function timenonce(o) { o.oauth_timestamp = ohauth.timestamp(); o.oauth_nonce = ohauth.nonce(); @@ -17,11 +13,12 @@ iD.OAuth = function() { } // token getter/setter, namespaced to the current `apibase` value. - function token(k, x) { - if (arguments.length == 2) { - localStorage[keyclean(baseurl) + k] = x; - } - return localStorage[keyclean(baseurl) + k]; + function token() { + return context.storage.apply(context, arguments); + } + + if (token('oauth_token')) { + o.oauth_token = token('oauth_token'); } oauth.authenticated = function() { @@ -63,7 +60,7 @@ iD.OAuth = function() { o.oauth_signature = ohauth.signature(oauth_secret, '', ohauth.baseString('POST', url, o)); - var l = iD.ui.loading('contacting openstreetmap...'); + var l = iD.ui.loading(context.container(), 'contacting openstreetmap...'); // it would make more sense to have this code within the callback // to oauth.xhr below. however, it needs to be directly within a @@ -112,7 +109,7 @@ iD.OAuth = function() { var request_token_secret = token('oauth_request_token_secret'); o.oauth_signature = ohauth.signature(oauth_secret, request_token_secret, ohauth.baseString('POST', url, o)); - var l = iD.ui.loading('contacting openstreetmap...'); + var l = iD.ui.loading(context.container(), 'contacting openstreetmap...'); function accessTokenDone(err, xhr) { if (err) callback(err); diff --git a/js/id/operations.js b/js/id/operations.js index 2786d046f..a72fe1d82 100644 --- a/js/id/operations.js +++ b/js/id/operations.js @@ -1 +1 @@ -iD.operations = {} +iD.operations = {}; diff --git a/js/id/operations/circular.js b/js/id/operations/circular.js deleted file mode 100644 index 0af64aa20..000000000 --- a/js/id/operations/circular.js +++ /dev/null @@ -1,34 +0,0 @@ -iD.operations.Circular = function(entityId, mode) { - var action = iD.actions.Circular(entityId, mode.map); - - var operation = function(history) { - var graph = history.graph(), - entity = graph.entity(entityId), - geometry = entity.geometry(graph); - - if (geometry === 'line') { - history.perform( - action, - 'made a line circular'); - - } else if (geometry === 'area') { - history.perform( - action, - 'made an area circular'); - } - }; - - operation.available = function(graph) { - var entity = graph.entity(entityId); - return entity.geometry(graph) === 'area' || entity.geometry(graph) === 'line'; - }; - - operation.enabled = function(graph) { - return action.enabled(graph); - }; - - operation.id = "circular"; - operation.title = "Circular"; - - return operation; -}; diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js new file mode 100644 index 000000000..2680da4ec --- /dev/null +++ b/js/id/operations/circularize.js @@ -0,0 +1,25 @@ +iD.operations.Circularize = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Circularize(entityId, context.projection); + + var operation = function() { + var annotation = t('operations.circularize.annotation.' + context.geometry(entityId)); + context.perform(action, annotation); + }; + + operation.available = function() { + return selection.length === 1 && + context.entity(entityId).type === 'way'; + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "circularize"; + operation.key = t('operations.circularize.key'); + operation.title = t('operations.circularize.title'); + operation.description = t('operations.circularize.description'); + + return operation; +}; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 53038dbc4..2c9765b62 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -1,34 +1,20 @@ -iD.operations.Delete = function(entityId) { - var operation = function(history) { - var graph = history.graph(), - entity = graph.entity(entityId), - geometry = entity.geometry(graph); +iD.operations.Delete = function(selection, context) { + var operation = function() { + var annotation; - if (geometry === 'vertex') { - history.perform( - iD.actions.DeleteNode(entityId), - 'deleted a vertex'); - - } else if (geometry === 'point') { - history.perform( - iD.actions.DeleteNode(entityId), - 'deleted a point'); - - } else if (geometry === 'line') { - history.perform( - iD.actions.DeleteWay(entityId), - 'deleted a line'); - - } else if (geometry === 'area') { - history.perform( - iD.actions.DeleteWay(entityId), - 'deleted an area'); + if (selection.length === 1) { + annotation = t('operations.delete.annotation.' + context.geometry(selection[0])); + } else { + annotation = t('operations.delete.annotation.multiple', {n: selection.length}); } + + context.perform( + iD.actions.DeleteMultiple(selection), + annotation); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); - return _.contains(['vertex', 'point', 'line', 'area'], entity.geometry(graph)); + operation.available = function() { + return true; }; operation.enabled = function() { @@ -36,7 +22,9 @@ iD.operations.Delete = function(entityId) { }; operation.id = "delete"; - operation.title = "Delete"; + operation.key = t('operations.delete.key'); + operation.title = t('operations.delete.title'); + operation.description = t('operations.delete.description'); return operation; }; diff --git a/js/id/operations/disconnect.js b/js/id/operations/disconnect.js new file mode 100644 index 000000000..c8cc0d014 --- /dev/null +++ b/js/id/operations/disconnect.js @@ -0,0 +1,24 @@ +iD.operations.Disconnect = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Disconnect(entityId); + + var operation = function() { + context.perform(action, t('operations.disconnect.annotation')); + }; + + operation.available = function() { + return selection.length === 1 && + context.geometry(entityId) === 'vertex'; + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "disconnect"; + operation.key = t('operations.disconnect.key'); + operation.title = t('operations.disconnect.title'); + operation.description = t('operations.disconnect.description'); + + return operation; +}; diff --git a/js/id/operations/merge.js b/js/id/operations/merge.js new file mode 100644 index 000000000..6202f1b3f --- /dev/null +++ b/js/id/operations/merge.js @@ -0,0 +1,34 @@ +iD.operations.Merge = function(selection, context) { + var join = iD.actions.Join(selection), + merge = iD.actions.Merge(selection); + + var operation = function() { + var annotation = t('operations.merge.annotation', {n: selection.length}), + action; + + if (join.enabled(context.graph())) { + action = join; + } else { + action = merge; + } + + var difference = context.perform(action, annotation); + context.enter(iD.modes.Select(context, difference.extantIDs())); + }; + + operation.available = function() { + return selection.length >= 2; + }; + + operation.enabled = function() { + return join.enabled(context.graph()) || + merge.enabled(context.graph()); + }; + + operation.id = "merge"; + operation.key = t('operations.merge.key'); + operation.title = t('operations.merge.title'); + operation.description = t('operations.merge.description'); + + return operation; +}; diff --git a/js/id/operations/move.js b/js/id/operations/move.js index 9806ede67..d9da39f64 100644 --- a/js/id/operations/move.js +++ b/js/id/operations/move.js @@ -1,10 +1,13 @@ -iD.operations.Move = function(entityId, mode) { +iD.operations.Move = function(selection, context) { + var entityId = selection[0]; + var operation = function() { - mode.controller.enter(iD.modes.MoveWay(entityId)); + context.enter(iD.modes.MoveWay(context, entityId)); }; - operation.available = function(graph) { - return graph.entity(entityId).type === 'way'; + operation.available = function() { + return selection.length === 1 && + context.entity(entityId).type === 'way'; }; operation.enabled = function() { @@ -12,7 +15,9 @@ iD.operations.Move = function(entityId, mode) { }; operation.id = "move"; - operation.title = "Move"; + operation.key = t('operations.move.key'); + operation.title = t('operations.move.title'); + operation.description = t('operations.move.description'); return operation; }; diff --git a/js/id/operations/orthogonalize.js b/js/id/operations/orthogonalize.js new file mode 100644 index 000000000..c59ed13e4 --- /dev/null +++ b/js/id/operations/orthogonalize.js @@ -0,0 +1,25 @@ +iD.operations.Orthogonalize = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Orthogonalize(entityId, context.projection); + + var operation = function() { + var annotation = t('operations.orthogonalize.annotation.' + context.geometry(entityId)); + context.perform(action, annotation); + }; + + operation.available = function() { + return selection.length === 1 && + context.entity(entityId).type === 'way'; + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "orthogonalize"; + operation.key = t('operations.orthogonalize.key'); + operation.title = t('operations.orthogonalize.title'); + operation.description = t('operations.orthogonalize.description'); + + return operation; +}; diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js index 941d8a4da..f750c134d 100644 --- a/js/id/operations/reverse.js +++ b/js/id/operations/reverse.js @@ -1,13 +1,15 @@ -iD.operations.Reverse = function(entityId) { - var operation = function(history) { - history.perform( - iD.actions.ReverseWay(entityId), - 'reversed a line'); +iD.operations.Reverse = function(selection, context) { + var entityId = selection[0]; + + var operation = function() { + context.perform( + iD.actions.Reverse(entityId), + t('operations.reverse.annotation')); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); - return entity.geometry(graph) === 'line'; + operation.available = function() { + return selection.length === 1 && + context.geometry(entityId) === 'line'; }; operation.enabled = function() { @@ -15,7 +17,9 @@ iD.operations.Reverse = function(entityId) { }; operation.id = "reverse"; - operation.title = "Reverse"; + operation.key = t('operations.reverse.key'); + operation.title = t('operations.reverse.title'); + operation.description = t('operations.reverse.description'); return operation; }; diff --git a/js/id/operations/split.js b/js/id/operations/split.js index 838dac88e..f49f4b9ed 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -1,21 +1,26 @@ -iD.operations.Split = function(entityId) { - var action = iD.actions.SplitWay(entityId); +iD.operations.Split = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Split(entityId); - var operation = function(history) { - history.perform(action, 'split a way'); + var operation = function() { + var annotation = t('operations.split.annotation'), + difference = context.perform(action, annotation); + context.enter(iD.modes.Select(context, difference.extantIDs())); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); - return entity.geometry(graph) === 'vertex'; + operation.available = function() { + return selection.length === 1 && + context.geometry(entityId) === 'vertex'; }; - operation.enabled = function(graph) { - return action.enabled(graph); + operation.enabled = function() { + return action.enabled(context.graph()); }; operation.id = "split"; - operation.title = "Split"; + operation.key = t('operations.split.key'); + operation.title = t('operations.split.title'); + operation.description = t('operations.split.description'); return operation; }; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js deleted file mode 100644 index 985a41697..000000000 --- a/js/id/operations/unjoin.js +++ /dev/null @@ -1,21 +0,0 @@ -iD.operations.Unjoin = function(entityId) { - var action = iD.actions.UnjoinNode(entityId); - - var operation = function(history) { - history.perform(action, 'unjoined lines'); - }; - - operation.available = function(graph) { - var entity = graph.entity(entityId); - return entity.geometry(graph) === 'vertex'; - }; - - operation.enabled = function(graph) { - return action.enabled(graph); - }; - - operation.id = "unjoin"; - operation.title = "Unjoin"; - - return operation; -}; diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 6476e7a28..8a14dc0f3 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -1,4 +1,10 @@ iD.Background = function() { + + var deviceRatio = (window.devicePixelRatio && + window.devicePixelRatio === 2) ? 0.5 : 1; + // tileSize = (deviceRatio === 0.5) ? [128,128] : [256,256]; + var tileSize = [256, 256]; + var tile = d3.geo.tile(), projection, cache = {}, @@ -6,14 +12,9 @@ iD.Background = function() { transformProp = iD.util.prefixCSSProperty('Transform'), source = d3.functor(''); - var imgstyle = 'position:absolute;transform-origin:0 0;' + - '-ms-transform-origin:0 0;' + - '-webkit-transform-origin:0 0;' + - '-moz-transform-origin:0 0;' + - '-o-transform-origin:0 0;' + - '-webkit-user-select: none;' + - '-webkit-user-drag: none;' + - '-moz-user-drag: none;'; + function tileSizeAtZoom(d, z) { + return Math.ceil(tileSize[0] * Math.pow(2, z - d[2])) / tileSize[0]; + } function atZoom(t, distance) { var power = Math.pow(2, distance); @@ -21,110 +22,110 @@ iD.Background = function() { Math.floor(t[0] * power), Math.floor(t[1] * power), t[2] + distance]; - az.push(source(az)); return az; } - function upZoom(t, distance) { - var az = atZoom(t, distance), tiles = []; - for (var x = 0; x < 2; x++) { - for (var y = 0; y < 2; y++) { - var up = [az[0] + x, az[1] + y, az[2]]; - up.push(source(up)); - tiles.push(up); - } - } - return tiles; - } - - function tileSize(d, z) { - return Math.ceil(256 * Math.pow(2, z - d[2])) / 256; - } - function lookUp(d) { for (var up = -1; up > -d[2]; up--) { if (cache[atZoom(d, up)] !== false) return atZoom(d, up); } } + function uniqueBy(a, n) { + var o = [], seen = {}; + for (var i = 0; i < a.length; i++) { + if (seen[a[i][n]] === undefined) { + o.push(a[i]); + seen[a[i][n]] = true; + } + } + return o; + } + + function addSource(d) { + d.push(source(d)); + return d; + } + // derive the tiles onscreen, remove those offscreen and position tiles // correctly for the currentstate of `projection` function background() { - var tiles = tile + var sel = this, + tiles = tile .scale(projection.scale()) - .scaleExtent(source.scaleExtent || [1, 17]) + .scaleExtent((source.data && source.data.scaleExtent) || [1, 17]) .translate(projection.translate())(), + requests = [], scaleExtent = tile.scaleExtent(), z = Math.max(Math.log(projection.scale()) / Math.log(2) - 8, 0), - rz = Math.max(scaleExtent[0], Math.min(scaleExtent[1], Math.floor(z))), - ts = 256 * Math.pow(2, z - rz), + rz = Math.max(scaleExtent[0], + Math.min(scaleExtent[1], Math.floor(z))), + ts = tileSize[0] * Math.pow(2, z - rz), tile_origin = [ projection.scale() / 2 - projection.translate()[0], - projection.scale() / 2 - projection.translate()[1]], - ups = {}; + projection.scale() / 2 - projection.translate()[1]]; tiles.forEach(function(d) { - - if (cache[d] === true) { - d.push(source(d)); - } else if (cache[d] === false && - cache[atZoom(d, -1)] !== false && - !ups[atZoom(d, -1)]) { - - ups[atZoom(d, -1)] = true; - tiles.push(atZoom(d, -1)); - - } else if (cache[d] === undefined && - lookUp(d)) { - - var upTile = lookUp(d); - if (!ups[upTile]) { - ups[upTile] = true; - tiles.push(upTile); - } - - } else if (cache[d] === undefined || - cache[d] === false) { - upZoom(d, 1).forEach(function(u) { - if (cache[u] && !ups[u]) { - ups[u] = true; - tiles.push(u); - } - }); + addSource(d); + requests.push(d); + if (!cache[d[3]] && lookUp(d)) { + requests.push(addSource(lookUp(d))); } }); - var image = this - .selectAll('img') - .data(tiles, function(d) { return d; }); + requests = uniqueBy(requests, 3).filter(function(r) { + // don't re-request tiles which have failed in the past + return cache[r[3]] !== false; + }); function load(d) { - cache[d.slice(0, 3)] = true; - d3.select(this).on('load', null); + cache[d[3]] = true; + d3.select(this) + .on('load', null) + .classed('tile-loaded', true); + background.apply(sel); } function error(d) { - cache[d.slice(0, 3)] = false; + cache[d[3]] = false; + d3.select(this).on('load', null); d3.select(this).remove(); + background.apply(sel); } + function imageTransform(d) { + var _ts = tileSize[0] * Math.pow(2, z - d[2]); + var scale = tileSizeAtZoom(d, z); + return 'translate(' + + (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' + + (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px)' + + 'scale(' + scale + ',' + scale + ')'; + } + + var image = this + .selectAll('img') + .data(requests, function(d) { return d[3]; }); + + image.exit() + .style(transformProp, imageTransform) + .classed('tile-loaded', false) + .each(function() { + var tile = this; + window.setTimeout(function() { + // this tile may already be removed + if (tile.parentNode) { + tile.parentNode.removeChild(tile); + } + }, 300); + }); + image.enter().append('img') - .attr('style', imgstyle) + .attr('class', 'tile') .attr('src', function(d) { return d[3]; }) .on('error', error) .on('load', load); - image.exit().remove(); - - image.style(transformProp, function(d) { - var _ts = 256 * Math.pow(2, z - d[2]); - var scale = tileSize(d, z); - return 'translate(' + - (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' + - (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px) scale(' + scale + ',' + scale + ')'; - }); - - if (Object.keys(cache).length > 100) cache = {}; + image.style(transformProp, imageTransform); } background.offset = function(_) { @@ -151,9 +152,22 @@ iD.Background = function() { return background; }; + function setPermalink(source) { + var tag = source.data.sourcetag; + var q = iD.util.stringQs(location.hash.substring(1)); + if (tag) { + location.replace('#' + iD.util.qsString(_.assign(q, { + layer: tag + }), true)); + } else { + location.replace('#' + iD.util.qsString(_.omit(q, 'layer'), true)); + } + } + background.source = function(_) { if (!arguments.length) return source; source = _; + setPermalink(source); return background; }; diff --git a/js/id/renderer/background_source.js b/js/id/renderer/background_source.js index cfed23294..3f624e1ba 100644 --- a/js/id/renderer/background_source.js +++ b/js/id/renderer/background_source.js @@ -1,8 +1,7 @@ iD.BackgroundSource = {}; // derive the url of a 'quadkey' style tile from a coordinate object -iD.BackgroundSource.template = function(template, subdomains, scaleExtent) { - scaleExtent = scaleExtent || [0, 18]; +iD.BackgroundSource.template = function(data) { var generator = function(coord) { var u = ''; for (var zoom = coord[2]; zoom > 0; zoom--) { @@ -12,18 +11,17 @@ iD.BackgroundSource.template = function(template, subdomains, scaleExtent) { if ((coord[1] & mask) !== 0) byte += 2; u += byte.toString(); } - // distribute requests against multiple domains - var t = subdomains ? - subdomains[coord[2] % subdomains.length] : ''; - return template - .replace('{t}', t) + + return data.template + .replace('{t}', data.subdomains ? + data.subdomains[coord[2] % data.subdomains.length] : '') .replace('{u}', u) .replace('{x}', coord[0]) .replace('{y}', coord[1]) .replace('{z}', coord[2]); }; - generator.scaleExtent = scaleExtent; + generator.data = data; return generator; }; @@ -31,21 +29,9 @@ iD.BackgroundSource.template = function(template, subdomains, scaleExtent) { iD.BackgroundSource.Custom = function() { var template = window.prompt('Enter a tile template. Valid tokens are {z}, {x}, {y} for Z/X/Y scheme and {u} for quadtile scheme.'); if (!template) return null; - return iD.BackgroundSource.template(template, null, [0, 20]); + return iD.BackgroundSource.template({ + template: template, + name: 'Custom (customized)' + }); }; - -iD.BackgroundSource.Bing = iD.BackgroundSource.template( - 'http://ecn.t{t}.tiles.virtualearth.net/tiles/a{u}.jpeg?g=587&mkt=en-gb&n=z', - [0, 1, 2, 3], [0, 20]); - -iD.BackgroundSource.Tiger2012 = iD.BackgroundSource.template( - 'http://{t}.tile.openstreetmap.us/tiger2012_roads_expanded/{z}/{x}/{y}.png', - ['a', 'b', 'c'], [0, 17]); - -iD.BackgroundSource.OSM = iD.BackgroundSource.template( - 'http://{t}.tile.openstreetmap.org/{z}/{x}/{y}.png', - ['a', 'b', 'c'], [0, 18]); - -iD.BackgroundSource.MapBox = iD.BackgroundSource.template( - 'http://{t}.tiles.mapbox.com/v3/openstreetmap.map-4wvf9l0l/{z}/{x}/{y}.jpg70', - ['a', 'b', 'c'], [0, 16]); +iD.BackgroundSource.Custom.data = { 'name': 'Custom' }; diff --git a/js/id/renderer/hash.js b/js/id/renderer/hash.js deleted file mode 100644 index 384705543..000000000 --- a/js/id/renderer/hash.js +++ /dev/null @@ -1,93 +0,0 @@ -iD.Hash = function() { - var hash = { hadHash: false }, - s0 = null, // cached location.hash - lat = 90 - 1e-8, // allowable latitude range - controller, - map; - - var parser = function(map, s) { - var q = iD.util.stringQs(s); - var args = (q.map || '').split("/").map(Number); - if (args.length < 3 || args.some(isNaN)) { - return true; // replace bogus hash - } else if (s !== formatter(map).slice(1)) { - map.centerZoom([args[2], - Math.min(lat, Math.max(-lat, args[1]))], - args[0]); - } - }; - - var formatter = function(map) { - var center = map.center(), - zoom = map.zoom(), - precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)); - var q = iD.util.stringQs(location.hash.substring(1)); - return '#' + iD.util.qsString(_.assign(q, { - map: zoom.toFixed(2) + - '/' + center[1].toFixed(precision) + - '/' + center[0].toFixed(precision) - }), true); - }; - - var move = _.throttle(function() { - var s1 = formatter(map); - if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! - }, 100); - - function hashchange() { - if (location.hash === s0) return; // ignore spurious hashchange events - if (parser(map, (s0 = location.hash).substring(1))) { - move(); // replace bogus hash - } - } - - // the hash can declare that the map should select a feature, but it can - // do so before any features are loaded. thus wait for the feature to - // be loaded and then select - function willselect(id) { - map.on('drawn.hash', function() { - var entity = map.history().graph().entity(id); - if (entity === undefined) return; - else selectoff(); - controller.enter(iD.modes.Select(entity)); - map.on('drawn.hash', null); - }); - controller.on('enter.hash', function() { - if (controller.mode.id !== 'browse') selectoff(); - }); - } - - function selectoff() { - map.on('drawn.hash', null); - } - - hash.controller = function(_) { - if (!arguments.length) return controller; - controller = _; - return hash; - }; - - hash.map = function(x) { - if (!arguments.length) return map; - if (map) { - map.on("move.hash", null); - window.removeEventListener("hashchange", hashchange, false); - } - map = x; - if (x) { - map.on("move.hash", move); - window.addEventListener("hashchange", hashchange, false); - if (location.hash) { - var q = iD.util.stringQs(location.hash.substring(1)); - if (q.id) { - willselect(q.id); - } - hashchange(); - hash.hadHash = true; - } - } - return hash; - }; - - return hash; -}; diff --git a/js/id/renderer/layers.js b/js/id/renderer/layers.js new file mode 100644 index 000000000..966e16bb8 --- /dev/null +++ b/js/id/renderer/layers.js @@ -0,0 +1,15 @@ +iD.layers = iD.data.imagery.map(iD.BackgroundSource.template); + +iD.layers.push((function() { + function custom() { + var template = window.prompt('Enter a tile template. Valid tokens are {z}, {x}, {y} for Z/X/Y scheme and {u} for quadtile scheme.'); + if (!template) return null; + if (template.match(/google/g)) return null; + return iD.BackgroundSource.template({ + template: template, + name: 'Custom (customized)' + }); + } + custom.data = { name: 'Custom' }; + return custom; +})()); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 60ad3de39..c3d5ec696 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -1,6 +1,5 @@ -iD.Map = function() { - var connection, history, - dimensions = [], +iD.Map = function(context) { + var dimensions = [], dispatch = d3.dispatch('move', 'drawn'), projection = d3.geo.mercator().scale(1024), roundedProjection = iD.svg.RoundProjection(projection), @@ -20,13 +19,15 @@ iD.Map = function() { vertices = iD.svg.Vertices(roundedProjection), lines = iD.svg.Lines(roundedProjection), areas = iD.svg.Areas(roundedProjection), - multipolygons = iD.svg.Multipolygons(roundedProjection), midpoints = iD.svg.Midpoints(roundedProjection), labels = iD.svg.Labels(roundedProjection), tail = d3.tail(), surface, tilegroup; function map(selection) { + context.history() + .on('change.map', redraw); + selection.call(zoom); tilegroup = selection.append('div') @@ -41,12 +42,16 @@ iD.Map = function() { d3.event.stopPropagation(); } }, true) + .on('mouseup.zoom', function() { + if (resetTransform()) redraw(); + }) .attr('id', 'surface') .call(iD.svg.Surface()); map.size(selection.size()); map.surface = surface; + map.tilesurface = tilegroup; supersurface .call(tail); @@ -55,49 +60,23 @@ iD.Map = function() { function pxCenter() { return [dimensions[0] / 2, dimensions[1] / 2]; } function drawVector(difference) { - if (surface.style(transformProp) != 'none') return; var filter, all, extent = map.extent(), - graph = history.graph(); - - function addParents(parents) { - for (var i = 0; i < parents.length; i++) { - var parent = parents[i]; - if (only[parent.id] === undefined) { - only[parent.id] = graph.entity(parent.id); - addParents(graph.parentRelations(parent)); - } - } - } + graph = context.graph(); if (!difference) { all = graph.intersects(extent); filter = d3.functor(true); } else { - var only = {}; - - for (var j = 0; j < difference.length; j++) { - var id = difference[j], - entity = graph.entity(id); - - // Even if the entity is false (deleted), it needs to be - // removed from the surface - only[id] = entity; - - if (entity && entity.intersects(extent, graph)) { - addParents(graph.parentWays(only[id])); - addParents(graph.parentRelations(only[id])); - } - } - - all = _.compact(_.values(only)); + var complete = difference.complete(extent); + all = _.compact(_.values(complete)); filter = function(d) { - if (d.midpoint) { + if (d.type === 'midpoint') { for (var i = 0; i < d.ways.length; i++) { - if (d.ways[i].id in only) return true; + if (d.ways[i].id in complete) return true; } } else { - return d.id in only; + return d.id in complete; } }; } @@ -110,9 +89,8 @@ iD.Map = function() { .call(vertices, graph, all, filter) .call(lines, graph, all, filter) .call(areas, graph, all, filter) - .call(multipolygons, graph, all, filter) .call(midpoints, graph, all, filter) - .call(labels, graph, all, filter, dimensions); + .call(labels, graph, all, filter, dimensions, !difference); } dispatch.drawn(map); } @@ -121,11 +99,6 @@ iD.Map = function() { surface.selectAll('.layer *').remove(); } - function connectionLoad(err, result) { - history.merge(result); - redraw(Object.keys(result.entities)); - } - function zoomPan() { if (d3.event && d3.event.sourceEvent.type === 'dblclick') { if (!dblclickEnabled) { @@ -136,10 +109,10 @@ iD.Map = function() { } if (Math.log(d3.event.scale / Math.LN2 - 8) < minzoom + 1) { - iD.flash() + iD.ui.flash(context.container()) .select('.content') .text('Cannot zoom out further in current mode.'); - return map.zoom(16); + return setZoom(16, true); } projection @@ -165,24 +138,34 @@ iD.Map = function() { } function resetTransform() { - if (!surface.style(transformProp)) return false; + var prop = surface.style(transformProp); + if (!prop || prop === 'none') return false; surface.style(transformProp, ''); tilegroup.style(transformProp, ''); return true; } function redraw(difference) { + clearTimeout(timeoutId); + // If we are in the middle of a zoom/pan, we can't do differenced redraws. // It would result in artifacts where differenced entities are redrawn with // one transform and unchanged entities with another. - if (resetTransform()) + if (resetTransform()) { difference = undefined; + } - surface.attr('data-zoom', ~~map.zoom()); - tilegroup.call(background); + var zoom = String(~~map.zoom()); + if (surface.attr('data-zoom') !== zoom) { + surface.attr('data-zoom', zoom); + } + + if (!difference) { + tilegroup.call(background); + } if (map.editable()) { - connection.loadTiles(projection, dimensions); + context.connection().loadTiles(projection, dimensions); drawVector(difference); } else { editOff(); @@ -195,7 +178,11 @@ iD.Map = function() { return map; } - var queueRedraw = _.debounce(redraw, 200); + var timeoutId; + function queueRedraw() { + clearTimeout(timeoutId); + timeoutId = setTimeout(function() { redraw(); }, 300); + } function pointLocation(p) { var translate = projection.translate(), @@ -230,8 +217,8 @@ iD.Map = function() { return map; }; - function setZoom(z) { - if (z === map.zoom()) + function setZoom(z, force) { + if (z === map.zoom() && !force) return false; var scale = 256 * Math.pow(2, z), center = pxCenter(), @@ -261,6 +248,15 @@ iD.Map = function() { return true; } + map.pan = function(d) { + var t = projection.translate(); + t[0] += d[0]; + t[1] += d[1]; + projection.translate(t); + zoom.translate(projection.translate()); + return redraw(); + }; + map.size = function(_) { if (!arguments.length) return dimensions; dimensions = _; @@ -318,39 +314,37 @@ iD.Map = function() { map.extent = function(_) { if (!arguments.length) { - return iD.geo.Extent(projection.invert([0, dimensions[1]]), + return new iD.geo.Extent(projection.invert([0, dimensions[1]]), projection.invert([dimensions[0], 0])); } else { - var extent = iD.geo.Extent(_), - tl = projection([extent[0][0], extent[1][1]]), - br = projection([extent[1][0], extent[0][1]]); - - // Calculate maximum zoom that fits extent - var hFactor = (br[0] - tl[0]) / dimensions[0], - vFactor = (br[1] - tl[1]) / dimensions[1], - hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2, - vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2, - newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff); - - map.centerZoom(extent.center(), newZoom); + var extent = iD.geo.Extent(_); + map.centerZoom(extent.center(), map.extentZoom(extent)); } }; - map.flush = function () { - connection.flush(); - history.reset(); - return map; + map.extentZoom = function(_) { + var extent = iD.geo.Extent(_), + tl = projection([extent[0][0], extent[1][1]]), + br = projection([extent[1][0], extent[0][1]]); + + // Calculate maximum zoom that fits extent + var hFactor = (br[0] - tl[0]) / dimensions[0], + vFactor = (br[1] - tl[1]) / dimensions[1], + hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2, + vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2, + newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff); + + return newZoom; }; - map.connection = function(_) { - if (!arguments.length) return connection; - connection = _; - connection.on('load.tile', connectionLoad); + map.flush = function() { + context.connection().flush(); + context.history().reset(); return map; }; var usedTails = {}; - map.tail = function (_, once) { + map.tail = function(_) { if (!_ || usedTails[_] === undefined) { tail.text(_); usedTails[_] = true; @@ -358,24 +352,6 @@ iD.Map = function() { return map; }; - map.hint = function (_) { - if (_ === false) { - d3.select('div.inspector-wrap') - .style('opacity', 0) - .style('display', 'none'); - } else { - d3.select('div.inspector-wrap') - .html('') - .style('display', 'block') - .transition() - .style('opacity', 1); - d3.select('div.inspector-wrap') - .append('div') - .attr('class','inspector-inner') - .text(_); - } - }; - map.editable = function() { return map.zoom() >= 16; }; @@ -386,13 +362,6 @@ iD.Map = function() { return map; }; - map.history = function (_) { - if (!arguments.length) return history; - history = _; - history.on('change.map', redraw); - return map; - }; - map.background = background; map.projection = projection; map.redraw = redraw; diff --git a/js/id/services/taginfo.js b/js/id/services/taginfo.js index aa89a37fa..850de9efd 100644 --- a/js/id/services/taginfo.js +++ b/js/id/services/taginfo.js @@ -40,7 +40,7 @@ iD.taginfo = function() { } function popularValues(parameters) { - return function(d) { return parseFloat(d['fraction']) > 0.01; }; + return function(d) { return parseFloat(d.fraction) > 0.01; }; } function valKey(d) { return { value: d.key }; } @@ -76,7 +76,7 @@ iD.taginfo = function() { page: 1 }, parameters)), function(err, d) { if (err) return callback(err); - callback(null, d.data.filter(popularValues()).map(valKeyDescription)); + callback(null, d.data.filter(popularValues()).map(valKeyDescription), parameters); }); }; diff --git a/js/id/svg.js b/js/id/svg.js index 5b07b3a53..36d1d060d 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -1,19 +1,21 @@ iD.svg = { - RoundProjection: function (projection) { - return function (d) { + RoundProjection: function(projection) { + return function(d) { return iD.geo.roundCoords(projection(d)); }; }, - PointTransform: function (projection) { - return function (entity) { - return 'translate(' + projection(entity.loc) + ')'; + PointTransform: function(projection) { + return function(entity) { + // http://jsperf.com/short-array-join + var pt = projection(entity.loc); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; }; }, - LineString: function (projection, graph) { + LineString: function(projection, graph) { var cache = {}; - return function (entity) { + return function(entity) { if (cache[entity.id] !== undefined) { return cache[entity.id]; } @@ -23,7 +25,22 @@ iD.svg = { } return (cache[entity.id] = - 'M' + graph.childNodes(entity).map(function (n) { return projection(n.loc); }).join('L')); - } + 'M' + graph.childNodes(entity).map(function(n) { + var pt = projection(n.loc); + return pt[0] + ',' + pt[1]; + }).join('L')); + }; + }, + + MultipolygonMemberTags: function(graph) { + return function(entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function(relation) { + if (relation.isMultipolygon()) { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + }; } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 991b5644c..46df5544c 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,38 +1,39 @@ iD.svg.Areas = function(projection) { return function drawAreas(surface, graph, entities, filter) { - var areas = []; + var path = d3.geo.path().projection(projection), + areas = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (entity.geometry(graph) === 'area') { - var points = graph.childNodes(entity).map(function(n) { - return projection(n.loc); - }); - areas.push({ entity: entity, - area: entity.isDegenerate() ? 0 : Math.abs(d3.geom.polygon(points).area()) + area: Math.abs(path.area(entity.asGeoJSON(graph))) }); } } areas.sort(function(a, b) { return b.area - a.area; }); - var lineString = iD.svg.LineString(projection, graph); + function drawPaths(group, areas, filter, klass) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } - function drawPaths(group, areas, filter, classes) { var paths = group.selectAll('path.area') .filter(filter) .data(areas, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', function(d) { return d.type + ' area ' + klass; }); paths .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) + .attr('d', function(entity) { return path(entity.asGeoJSON(graph)); }) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -43,9 +44,14 @@ iD.svg.Areas = function(projection) { areas = _.pluck(areas, 'entity'); + var strokes = areas.filter(function(area) { + return area.type === 'way'; + }); + var fill = surface.select('.layer-fill'), - stroke = surface.select('.layer-stroke'), - fills = drawPaths(fill, areas, filter, 'way area fill'), - strokes = drawPaths(stroke, areas, filter, 'way area stroke'); + stroke = surface.select('.layer-stroke'); + + drawPaths(fill, areas, filter, 'fill'); + drawPaths(stroke, strokes, filter, 'stroke'); }; }; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 2fcc2fcc1..507f4cbdd 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -2,21 +2,42 @@ iD.svg.Labels = function(projection) { // Replace with dict and iterate over entities tags instead? var label_stack = [ + ['line', 'aeroway'], ['line', 'highway'], - ['area', 'building', 'yes'], - ['area', 'leisure', 'park'], + ['line', 'railway'], + ['line', 'waterway'], + ['area', 'aeroway'], + ['area', 'amenity'], + ['area', 'building'], + ['area', 'historic'], + ['area', 'leisure'], + ['area', 'man_made'], ['area', 'natural'], + ['area', 'shop'], + ['area', 'tourism'], + ['point', 'aeroway'], ['point', 'amenity'], - ['point', 'shop'] + ['point', 'building'], + ['point', 'historic'], + ['point', 'leisure'], + ['point', 'man_made'], + ['point', 'natural'], + ['point', 'shop'], + ['point', 'tourism'], + ['line', 'name'], + ['area', 'name'], + ['point', 'name'] ]; var default_size = 12; var font_sizes = label_stack.map(function(d) { - var style = iD.util.getStyle( - 'text.' + d[0] + '.tag-' + d.slice(1).join('-')); + var style = iD.util.getStyle('text.' + d[0] + '.tag-' + d[1]); var m = style && style.cssText.match("font-size: ([0-9]{1,2})px;"); - if (!m) return default_size; - return parseInt(m[1], 10); + if (m) return parseInt(m[1], 10); + style = iD.util.getStyle('text.' + d[0]); + m = style && style.cssText.match("font-size: ([0-9]{1,2})px;"); + if (m) return parseInt(m[1], 10); + return default_size; }); var pointOffsets = [ @@ -61,7 +82,7 @@ iD.svg.Labels = function(projection) { 'startOffset': '50%', 'xlink:href': function(d, i) { return '#halo-' + d.id; } }) - .text(function(d, i) { return d.tags.name; }); + .text(function(d, i) { return name(d); }); texts.exit().remove(); @@ -98,14 +119,14 @@ iD.svg.Labels = function(projection) { 'x': function(d, i) { var x = labels[i].x - 2; if (labels[i].textAnchor === 'middle') { - x -= textWidth(d.tags.name, labels[i].height) / 2; + x -= textWidth(name(d), labels[i].height) / 2; } return x; }, 'y': function(d, i) { return labels[i].y - labels[i].height + 1 - 2; }, 'rx': 3, 'ry': 3, - 'width': function(d, i) { return textWidth(d.tags.name, labels[i].height) + 4; }, + 'width': function(d, i) { return textWidth(name(d), labels[i].height) + 4; }, 'height': function(d, i) { return labels[i].height + 4; }, 'fill': 'white' }); @@ -128,8 +149,8 @@ iD.svg.Labels = function(projection) { .attr('y', get(labels, 'y')) .attr('transform', get(labels, 'transform')) .style('text-anchor', get(labels, 'textAnchor')) - .text(function(d) { return d.tags.name; }) - .each(function(d, i) { textWidth(d.tags.name, labels[i].height, this); }); + .text(function(d) { return name(d); }) + .each(function(d, i) { textWidth(name(d), labels[i].height, this); }); texts.exit().remove(); return texts; @@ -156,7 +177,7 @@ iD.svg.Labels = function(projection) { for (var i = 0; i < nodes.length - 1; i++) { var current = segmentLength(i); var portion; - if (!start && sofar + current > from) { + if (!start && sofar + current >= from) { portion = (from - sofar) / current; start = [ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), @@ -164,7 +185,7 @@ iD.svg.Labels = function(projection) { ]; i0 = i + 1; } - if (!end && sofar + current > to) { + if (!end && sofar + current >= to) { portion = (to - sofar) / current; end = [ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), @@ -183,29 +204,82 @@ iD.svg.Labels = function(projection) { } - return function drawLabels(surface, graph, entities, filter, dimensions) { + function hideOnMouseover() { + var mouse = mousePosition(d3.event), + pad = 50, + rect = new RTree.Rectangle(mouse[0] - pad, mouse[1] - pad, 2*pad, 2*pad), + labels = _.pluck(rtree.search(rect, this), 'leaf'), + containsLabel = iD.util.trueObj(labels), + selection = d3.select(this); - var rtree = new RTree(); - var hidePoints = !d3.select('.node.point').node(); + // ensures that simply resetting opacity + // does not force style recalculation + function resetOpacity() { + if (this._opacity !== '') { + this.style.opacity = ''; + this._opacity = ''; + } + } + + selection.selectAll('.layer-label text, .layer-halo path, .layer-halo rect') + .each(resetOpacity); + + if (!labels.length) return; + selection.selectAll('.layer-label text, .layer-halo path, .layer-halo rect') + .filter(function(d) { + return containsLabel[d.id]; + }) + .style('opacity', 0) + .property('_opacity', 0); + } + + function name(d) { + return d.tags[lang] || d.tags.name; + } + + var rtree = new RTree(), + rectangles = {}, + lang = 'name:' + iD.detect().locale.toLowerCase().split('-')[0], + mousePosition, cacheDimensions; + + return function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) { + + if (!mousePosition || dimensions.join(',') !== cacheDimensions) { + mousePosition = iD.util.fastMouse(surface.node().parentNode); + cacheDimensions = dimensions.join(','); + } + + d3.select(surface.node().parentNode) + .on('mousemove.hidelabels', hideOnMouseover); + + var hidePoints = !surface.select('.node.point').node(); var labelable = [], i, k, entity; for (i = 0; i < label_stack.length; i++) labelable.push([]); + if (fullRedraw) { + rtree = new RTree(); + rectangles = {}; + } else { + for (i = 0; i < entities.length; i++) { + rtree.remove(rectangles[entities[i].id], entities[i].id); + } + } + // Split entities into groups specified by label_stack for (i = 0; i < entities.length; i++) { entity = entities[i]; - if (!entity.tags.name) continue; + if (!name(entity)) continue; if (hidePoints && entity.geometry(graph) === 'point') continue; for (k = 0; k < label_stack.length; k ++) { if (entity.geometry(graph) === label_stack[k][0] && - entity.tags[label_stack[k][1]] && !entity.tags[label_stack[k][2]]) { + entity.tags[label_stack[k][1]]) { labelable[k].push(entity); break; } } } - var positions = { point: [], line: [], @@ -223,7 +297,7 @@ iD.svg.Labels = function(projection) { var font_size = font_sizes[k]; for (i = 0; i < labelable[k].length; i ++) { entity = labelable[k][i]; - var width = textWidth(entity.tags.name, font_size), + var width = textWidth(name(entity), font_size), p; if (entity.geometry(graph) === 'point') { p = getPointLabel(entity, width, font_size); @@ -233,7 +307,7 @@ iD.svg.Labels = function(projection) { p = getAreaLabel(entity, width, font_size); } if (p) { - p.classes = entity.geometry(graph) + ' tag-' + label_stack[k].slice(1).join('-'); + p.classes = entity.geometry(graph) + ' tag-' + label_stack[k][1]; positions[entity.geometry(graph)].push(p); labelled[entity.geometry(graph)].push(entity); } @@ -252,7 +326,7 @@ iD.svg.Labels = function(projection) { textAnchor: offset[2] }; var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m); - if (tryInsert(rect)) return p; + if (tryInsert(rect, entity.id)) return p; } @@ -275,7 +349,7 @@ iD.svg.Labels = function(projection) { Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30 ); if (rev) sub = sub.reverse(); - if (tryInsert(rect)) return { + if (tryInsert(rect, entity.id)) return { 'font-size': height + 2, lineString: lineString(sub), startOffset: offset + '%' @@ -284,9 +358,8 @@ iD.svg.Labels = function(projection) { } function getAreaLabel(entity, width, height) { - var nodes = _.pluck(graph.childNodes(entity), 'loc') - .map(iD.svg.RoundProjection(projection)), - centroid = d3.geom.polygon(nodes).centroid(), + var path = d3.geo.path().projection(projection), + centroid = path.centroid(entity.asGeoJSON(graph)), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; @@ -298,16 +371,19 @@ iD.svg.Labels = function(projection) { height: height }; var rect = new RTree.Rectangle(p.x - width/2, p.y, width, height); - if (tryInsert(rect)) return p; + if (tryInsert(rect, entity.id)) return p; } - function tryInsert(rect) { + function tryInsert(rect, id) { // Check that label is visible if (rect.x1 < 0 || rect.y1 < 0 || rect.x2 > dimensions[0] || rect.y2 > dimensions[1]) return false; var v = rtree.search(rect, true).length === 0; - if (v) rtree.insert(rect); + if (v) { + rtree.insert(rect, id); + rectangles[id] = rect; + } return v; } diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 65cfccd27..59fec46e1 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -1,6 +1,6 @@ iD.svg.Lines = function(projection) { - var arrowtext = '►\u3000\u3000', + var arrowtext = '►\u3000\u3000\u3000', alength; var highway_stack = { @@ -34,19 +34,25 @@ iD.svg.Lines = function(projection) { } return function drawLines(surface, graph, entities, filter) { - function drawPaths(group, lines, filter, classes, lineString) { - var paths = group.selectAll('path') + function drawPaths(group, lines, filter, klass, lineString) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } + + var paths = group.selectAll('path.line') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', 'way line ' + klass); paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -56,13 +62,16 @@ iD.svg.Lines = function(projection) { } if (!alength) { - var arrow = surface.append('text').text(arrowtext); + var container = surface.append('g') + .attr('class', 'oneway'), + arrow = container.append('text') + .attr('class', 'textpath') + .text(arrowtext); alength = arrow.node().getComputedTextLength(); - arrow.remove(); + container.remove(); } - var lines = [], - lineStrings = {}; + var lines = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -80,13 +89,13 @@ iD.svg.Lines = function(projection) { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); + shadows = drawPaths(shadow, lines, filter, 'shadow', lineString), + casings = drawPaths(casing, lines, filter, 'casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, - oneways = strokes.filter(function (d) { return d.isOneWay(); }).each(function(d) { + oneways = strokes.filter(function(d) { return d.isOneWay(); }).each(function(d) { lengths[d.id] = Math.floor(this.getTotalLength() / alength); }).data(); diff --git a/js/id/svg/member_classes.js b/js/id/svg/member_classes.js index d5745a466..713cc898e 100644 --- a/js/id/svg/member_classes.js +++ b/js/id/svg/member_classes.js @@ -17,7 +17,7 @@ iD.svg.MemberClasses = function(graph) { classes += ' member'; } - relations.forEach(function (relation) { + relations.forEach(function(relation) { classes += ' member-type-' + relation.tags.type; classes += ' member-role-' + relation.memberById(d.id).role; }); diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js index 655e35013..fc1a5bbc3 100644 --- a/js/id/svg/midpoints.js +++ b/js/id/svg/midpoints.js @@ -2,6 +2,10 @@ iD.svg.Midpoints = function(projection) { return function drawMidpoints(surface, graph, entities, filter) { var midpoints = {}; + if (!surface.select('.layer-hit g.vertex').node()) { + return surface.selectAll('.layer-hit g.midpoint').remove(); + } + for (var i = 0; i < entities.length; i++) { if (entities[i].type !== 'way') continue; @@ -15,19 +19,15 @@ iD.svg.Midpoints = function(projection) { b = nodes[j + 1], id = [a.id, b.id].sort().join('-'); - if (!midpoints[id] && - iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) { - - var midpoint_loc = iD.geo.interp(a.loc, b.loc, 0.5), - parents = _.intersection(graph.parentWays(a), - graph.parentWays(b)); + if (midpoints[id]) { + midpoints[id].ways.push({id: entity.id, index: j + 1}); + } else if (iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) { midpoints[id] = { - loc: midpoint_loc, - ways: parents, - nodes: [a.id, b.id], + type: 'midpoint', id: id, - midpoint: true + loc: iD.geo.interp(a.loc, b.loc, 0.5), + ways: [{id: entity.id, index: j + 1}] }; } } @@ -35,14 +35,14 @@ iD.svg.Midpoints = function(projection) { var groups = surface.select('.layer-hit').selectAll('g.midpoint') .filter(filter) - .data(_.values(midpoints), function (d) { return [d.parents, d.id].join(","); }); + .data(_.values(midpoints), function(d) { return d.id; }); var group = groups.enter() .insert('g', ':first-child') .attr('class', 'midpoint'); group.append('circle') - .attr('r', 7) + .attr('r', 8) .attr('class', 'shadow'); group.append('circle') @@ -51,6 +51,10 @@ iD.svg.Midpoints = function(projection) { groups.attr('transform', iD.svg.PointTransform(projection)); + // Propagate data bindings. + groups.select('circle.shadow'); + groups.select('circle.fill'); + groups.exit() .remove(); }; diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js deleted file mode 100644 index b4f050ad5..000000000 --- a/js/id/svg/multipolygons.js +++ /dev/null @@ -1,55 +0,0 @@ -iD.svg.Multipolygons = function(projection) { - return function(surface, graph, entities, filter) { - var multipolygons = []; - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (entity.geometry(graph) === 'relation' && entity.tags.type === 'multipolygon') { - multipolygons.push(entity); - } - } - - var lineStrings = {}; - - function lineString(entity) { - if (lineStrings[entity.id] !== undefined) { - return lineStrings[entity.id]; - } - - var multipolygon = entity.multipolygon(graph); - if (entity.members.length === 0 || !multipolygon) { - return (lineStrings[entity.id] = null); - } - - multipolygon = _.flatten(multipolygon, true); - return (lineStrings[entity.id] = - multipolygon.map(function (ring) { - return 'M' + ring.map(function (node) { return projection(node.loc); }).join('L'); - }).join("")); - } - - function drawPaths(group, multipolygons, filter, classes) { - var paths = group.selectAll('path.multipolygon') - .filter(filter) - .data(multipolygons, iD.Entity.key); - - paths.enter() - .append('path') - .attr('class', classes); - - paths - .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) - .call(iD.svg.MemberClasses(graph)); - - paths.exit() - .remove(); - - return paths; - } - - var fill = surface.select('.layer-fill'), - paths = drawPaths(fill, multipolygons, filter, 'relation multipolygon'); - }; -}; diff --git a/js/id/svg/points.js b/js/id/svg/points.js index 471bc4b4d..838faf2bb 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -52,6 +52,7 @@ iD.svg.Points = function(projection) { // sets the data (point entity) on the element groups.select('image') .attr('xlink:href', imageHref); + groups.select('.shadow, .stroke'); groups.exit() .remove(); @@ -79,7 +80,7 @@ iD.svg.Points.imageIndex = [ }, { tags: { man_made: 'lighthouse' }, - icon: 'lighthouselevel_crossing' + icon: 'lighthouse' }, { tags: { natural: 'peak' }, diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index 7561802e5..a11f8fbae 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -3,7 +3,7 @@ iD.svg.Surface = function() { selection.append('defs'); var layers = selection.selectAll('.layer') - .data(['shadow', 'fill', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']); + .data(['fill', 'shadow', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js index 5ce585add..dad388ffe 100644 --- a/js/id/svg/tag_classes.js +++ b/js/id/svg/tag_classes.js @@ -3,10 +3,11 @@ iD.svg.TagClasses = function() { 'highway', 'railway', 'waterway', 'power', 'motorway', 'amenity', 'natural', 'landuse', 'building', 'oneway', 'bridge', 'boundary', 'leisure', 'construction' - ]), tagClassRe = /^tag-/; + ]), tagClassRe = /^tag-/, + tags = function(entity) { return entity.tags; }; - return function tagClassesSelection(selection) { - selection.each(function tagClassesEach(d, i) { + var tagClasses = function(selection) { + selection.each(function tagClassesEach(entity) { var classes, value = this.className; if (value.baseVal !== undefined) value = value.baseVal; @@ -15,11 +16,10 @@ iD.svg.TagClasses = function() { return name.length && !tagClassRe.test(name); }).join(' '); - var tags = d.tags; - for (var k in tags) { + var t = tags(entity); + for (var k in t) { if (!keys[k]) continue; - classes += ' tag-' + k + ' ' + - 'tag-' + k + '-' + tags[k]; + classes += ' tag-' + k + ' ' + 'tag-' + k + '-' + t[k]; } classes = classes.trim(); @@ -29,4 +29,12 @@ iD.svg.TagClasses = function() { } }); }; + + tagClasses.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return tagClasses; + }; + + return tagClasses; }; diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index 06be536ec..339c0752a 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -23,25 +23,24 @@ iD.svg.Vertices = function(projection) { group.append('circle') .attr('r', 10) - .attr('class', 'shadow'); + .attr('class', 'node vertex shadow'); group.append('circle') - .attr('r', 6) - .attr('class', 'stroke'); + .attr('r', 4) + .attr('class', 'node vertex stroke'); group.append('circle') .attr('r', 3) - .attr('class', 'fill'); + .attr('class', 'node vertex fill'); groups.attr('transform', iD.svg.PointTransform(projection)) .call(iD.svg.TagClasses()) .call(iD.svg.MemberClasses(graph)) - .classed('shared', function(entity) { return graph.parentWays(entity).length > 1; }); + .classed('shared', function(entity) { return graph.isShared(entity); }); // Selecting the following implicitly // sets the data (vertix entity) on the elements - groups.select('circle.fill'); - groups.select('circle.stroke'); + groups.select('circle.fill, circle.stroke, circle.shadow'); groups.exit() .remove(); diff --git a/js/id/ui.js b/js/id/ui.js index 8901175f4..3db4f6ece 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -1 +1,144 @@ -iD.ui = {}; +iD.ui = function(context) { + return function(container) { + context.container(container); + + var history = context.history(), + map = context.map(); + + if (!iD.detect().support) { + container + .text(t('browser_notice')) + .style({ + 'text-align': 'center', + 'font-style': 'italic' + }); + return; + } + + if (iD.detect().opera) container.classed('opera', true); + + var m = container.append('div') + .attr('id', 'map') + .call(map); + + var bar = container.append('div') + .attr('id', 'bar') + .attr('class','pad1 fillD'); + + var limiter = bar.append('div') + .attr('class', 'limiter'); + + limiter.append('div') + .attr('class', 'button-wrap joined col4') + .call(iD.ui.Modes(context), limiter); + + limiter.append('div') + .attr('class', 'button-wrap joined col1') + .call(iD.ui.UndoRedo(context)); + + limiter.append('div') + .attr('class', 'button-wrap col1') + .call(iD.ui.Save(context)); + + container.append('div') + .attr('class', 'map-control zoombuttons') + .call(iD.ui.Zoom(context)); + + container.append('div') + .attr('class', 'map-control geocode-control') + .call(iD.ui.Geocoder(context)); + + container.append('div') + .attr('class', 'map-control layerswitcher-control') + .call(iD.ui.LayerSwitcher(context)); + + container.append('div') + .attr('class', 'map-control geolocate-control') + .call(iD.ui.Geolocate(map)); + + container.append('div') + .style('display', 'none') + .attr('class', 'inspector-wrap fr col5'); + + var about = container.append('div') + .attr('class','col12 about-block fillD pad1'); + + about.append('div') + .attr('class', 'account') + .call(iD.ui.Account(context)); + + var linkList = about.append('ul') + .attr('id', 'about') + .attr('class', 'pad1 fillD about-block link-list'); + + linkList.append('li') + .append('a') + .attr('target', '_blank') + .attr('href', 'http://github.com/systemed/iD') + .text(iD.version); + + linkList.append('li') + .append('a') + .attr('target', '_blank') + .attr('href', 'http://github.com/systemed/iD/issues') + .text(t('report_a_bug')); + + linkList.append('li') + .attr('class', 'attribution') + .call(iD.ui.Attribution(context)); + + linkList.append('li') + .attr('class', 'source-switch') + .call(iD.ui.SourceSwitch(context)); + + linkList.append('li') + .attr('class', 'user-list') + .call(iD.ui.Contributors(context)); + + window.onbeforeunload = function() { + history.save(); + if (history.hasChanges()) return t('save.unsaved_changes'); + }; + + d3.select(window).on('resize.editor', function() { + map.size(m.size()); + }); + + function pan(d) { + return function() { + context.pan(d); + }; + } + + // pan amount + var pa = 5; + + var keybinding = d3.keybinding('main') + .on('⌫', function() { d3.event.preventDefault(); }) + .on('←', pan([pa, 0])) + .on('↑', pan([0, pa])) + .on('→', pan([-pa, 0])) + .on('↓', pan([0, -pa])); + + d3.select(document) + .call(keybinding); + + var hash = iD.behavior.Hash(context); + + hash(); + + if (!hash.hadHash) { + map.centerZoom([-77.02271, 38.90085], 20); + } + + context.enter(iD.modes.Browse(context)); + + context.container() + .call(iD.ui.Splash(context)) + .call(iD.ui.Restore(context)); + }; +}; + +iD.ui.tooltipHtml = function(text, key) { + return '' + text + '' + '
' + key + '
'; +}; diff --git a/js/id/ui/account.js b/js/id/ui/account.js new file mode 100644 index 000000000..5e9fe7705 --- /dev/null +++ b/js/id/ui/account.js @@ -0,0 +1,53 @@ +iD.ui.Account = function(context) { + var connection = context.connection(); + + function update(selection) { + if (!connection.authenticated()) { + selection.html('') + .style('display', 'none'); + return; + } + + selection.style('display', 'block'); + + connection.userDetails(function(err, details) { + selection.html(''); + + if (err) return; + + // Link + var userLink = selection.append('a') + .attr('href', connection.url() + '/user/' + details.display_name) + .attr('target', '_blank'); + + // Add thumbnail or dont + if (details.image_url) { + userLink.append('img') + .attr('class', 'icon icon-pre-text user-icon') + .attr('src', details.image_url); + } else { + userLink.append('span') + .attr('class', 'icon avatar icon-pre-text'); + } + + // Add user name + userLink.append('span') + .attr('class', 'label') + .text(details.display_name); + + selection.append('a') + .attr('class', 'logout') + .attr('href', '#') + .text(t('logout')) + .on('click.logout', function() { + d3.event.preventDefault(); + connection.logout(); + }); + }); + } + + return function(selection) { + connection.on('auth', function() { update(selection); }); + update(selection); + }; +}; diff --git a/js/id/ui/attribution.js b/js/id/ui/attribution.js new file mode 100644 index 000000000..282833e7e --- /dev/null +++ b/js/id/ui/attribution.js @@ -0,0 +1,10 @@ +iD.ui.Attribution = function(context) { + return function(selection) { + selection.append('span') + .text('imagery'); + + selection + .append('span') + .attr('class', 'provided-by'); + } +}; diff --git a/js/id/ui/cmd.js b/js/id/ui/cmd.js new file mode 100644 index 000000000..9876da236 --- /dev/null +++ b/js/id/ui/cmd.js @@ -0,0 +1,22 @@ +// Translate a MacOS key command into the appropriate Windows/Linux equivalent. +// For example, ⌘Z -> Ctrl+Z +iD.ui.cmd = function(code) { + if (iD.detect().os === 'mac') + return code; + + var modifiers = { + '⌘': 'Ctrl', + '⇧': 'Shift', + '⌥': 'Alt' + }, keys = []; + + for (var i = 0; i < code.length; i++) { + if (code[i] in modifiers) { + keys.push(modifiers[code[i]]); + } else { + keys.push(code[i]); + } + } + + return keys.join('+'); +}; diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index 29cf64b2f..da6fe5c02 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -1,4 +1,4 @@ -iD.ui.commit = function(map) { +iD.ui.Commit = function(context) { var event = d3.dispatch('cancel', 'save', 'fix'); function zipSame(d) { @@ -31,111 +31,117 @@ iD.ui.commit = function(map) { header = selection.append('div').attr('class', 'header modal-section fillL'), body = selection.append('div').attr('class', 'body'); - header.append('h2').text('Save Changes'); + header.append('h2') + .text(t('commit.title')); - // Comment Box - var comment_section = body.append('div').attr('class','modal-section fillD'); - comment_section.append('textarea') + var commentSection = body.append('div') + .attr('class', 'modal-section fillD'); + + var commentField = commentSection.append('textarea') .attr('class', 'changeset-comment') - .attr('placeholder', 'Brief Description of your contributions') - .property('value', localStorage.comment || '') - .node().select(); + .attr('placeholder', t('commit.description_placeholder')) + .property('value', context.storage('comment') || ''); - var commit_info = - comment_section - .append('p') - .attr('class','commit-info'); + commentField.node().select(); - commit_info.append('span').text('The changes you upload as '); + var userLink = d3.select(document.createElement('div')); - var user_link = commit_info.append('a') - .attr('class','user-info') - .text(user.display_name) - .attr('href', connection.url() + '/user/' + user.display_name) - .attr('target', '_blank'); - - commit_info.append('span').text(' will be visible on all maps that use OpenStreetMap data:'); + userLink.append('a') + .attr('class','user-info') + .text(user.display_name) + .attr('href', connection.url() + '/user/' + user.display_name) + .attr('target', '_blank'); if (user.image_url) { - user_link - .append('img') - .attr('src', user.image_url) - .attr('class', 'icon icon-pre-text user-icon'); + userLink.append('img') + .attr('src', user.image_url) + .attr('class', 'icon icon-pre-text user-icon'); } - // Confirm / Cancel Buttons - var buttonwrap = comment_section.append('div') - .attr('class', 'buttons cf') - .append('div') - .attr('class', 'button-wrap joined col4'); + commentSection.append('p') + .attr('class', 'commit-info') + .html(t('commit.upload_explanation', {user: userLink.html()})); - var savebutton = buttonwrap - .append('button') + // Confirm / Cancel Buttons + var buttonWrap = commentSection.append('div') + .attr('class', 'buttons cf') + .append('div') + .attr('class', 'button-wrap joined col4'); + + var saveButton = buttonWrap.append('button') .attr('class', 'save action col6 button') .on('click.save', function() { - var comment = d3.select('textarea.changeset-comment').node().value; + var comment = commentField.node().value; localStorage.comment = comment; event.save({ comment: comment }); }); - savebutton.append('span').attr('class','label').text('Save'); - var cancelbutton = buttonwrap.append('button') + saveButton.append('span') + .attr('class', 'label') + .text(t('commit.save')); + + var cancelButton = buttonWrap.append('button') .attr('class', 'cancel col6 button') .on('click.cancel', function() { event.cancel(); }); - cancelbutton.append('span').attr('class','icon close icon-pre-text'); - cancelbutton.append('span').attr('class','label').text('Cancel'); + + cancelButton.append('span') + .attr('class', 'label') + .text(t('commit.cancel')); var warnings = body.selectAll('div.warning-section') - .data(iD.validate(changes, map.history().graph())) + .data(iD.validate(changes, context.graph())) .enter() - .append('div').attr('class', 'modal-section warning-section fillL'); + .append('div') + .attr('class', 'modal-section warning-section fillL'); warnings.append('h3') - .text('Warnings'); + .text(t('commit.warnings')); - var warning_li = warnings.append('ul') + var warningLi = warnings.append('ul') .attr('class', 'changeset-list') .selectAll('li') .data(function(d) { return d; }) .enter() .append('li'); - warning_li.append('button') + warningLi.append('button') .attr('class', 'minor') .on('click', event.fix) .append('span') .attr('class', 'icon warning'); - warning_li.append('strong').text(function(d) { + warningLi.append('strong').text(function(d) { return d.message; }); var section = body.selectAll('div.commit-section') .data(['modified', 'deleted', 'created'].filter(changesLength)) .enter() - .append('div').attr('class', 'commit-section modal-section fillL2'); + .append('div') + .attr('class', 'commit-section modal-section fillL2'); - section.append('h3').text(function(d) { - return d.charAt(0).toUpperCase() + d.slice(1); - }) + section.append('h3') + .text(function(d) { return t('commit.' + d); }) .append('small') .attr('class', 'count') .text(changesLength); var li = section.append('ul') - .attr('class','changeset-list') + .attr('class', 'changeset-list') .selectAll('li') .data(function(d) { return zipSame(changes[d]); }) .enter() .append('li'); - li.append('strong').text(function(d) { - return (d.count > 1) ? d.type + 's ' : d.type + ' '; - }); + li.append('strong') + .text(function(d) { + return (d.count > 1) ? d.type + 's ' : d.type + ' '; + }); + li.append('span') .text(function(d) { return d.name; }) .attr('title', function(d) { return d.tagText; }); diff --git a/js/id/ui/confirm.js b/js/id/ui/confirm.js index 02133406b..177bb94f0 100644 --- a/js/id/ui/confirm.js +++ b/js/id/ui/confirm.js @@ -1,5 +1,5 @@ -iD.ui.confirm = function() { - var modal = iD.ui.modal(); +iD.ui.confirm = function(selection) { + var modal = iD.ui.modal(selection); modal.select('.modal').classed('modal-alert', true); modal.select('.content') .attr('class','modal-section fillD') diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index c0036a677..2389f266a 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -1,10 +1,8 @@ -iD.ui.contributors = function(map) { - - function contributors(selection) { - +iD.ui.Contributors = function(context) { + function update(selection) { var users = {}, limit = 3, - entities = map.history().graph().intersects(map.extent()); + entities = context.graph().intersects(context.map().extent()); for (var i in entities) { if (entities[i].user) users[entities[i].user] = true; @@ -13,36 +11,39 @@ iD.ui.contributors = function(map) { var u = Object.keys(users), subset = u.slice(0, limit); - var l = selection - .select('.contributor-list') - .selectAll('a.user-link') - .data(subset); + selection.html('') + .append('span') + .attr('class', 'icon nearby icon-pre-text'); + var userList = d3.select(document.createElement('span')); - l.enter().append('a') + userList.selectAll() + .data(subset) + .enter() + .append('a') .attr('class', 'user-link') - .attr('href', function(d) { return map.connection().userUrl(d); }) + .attr('href', function(d) { return context.connection().userUrl(d); }) .attr('target', '_blank') .text(String); - l.exit().remove(); - - selection - .select('.contributor-count') - .html(''); - if (u.length > limit) { - selection - .select('.contributor-count') - .append('a') + var count = d3.select(document.createElement('span')); + + count.append('a') .attr('target', '_blank') .attr('href', function() { - var ext = map.extent(); + var ext = context.map().extent(); return 'http://www.openstreetmap.org/browse/changesets?bbox=' + [ ext[0][0], ext[0][1], ext[1][0], ext[1][1]]; }) - .text(' and ' + (u.length - limit) + ' others'); + .text(u.length - limit); + + selection.append('span') + .html(t('contributors.truncated_list', {users: userList.html(), count: count.html()})); + } else { + selection.append('span') + .html(t('contributors.list', {users: userList.html()})); } if (!u.length) { @@ -50,9 +51,17 @@ iD.ui.contributors = function(map) { } else if (selection.style('opacity') === '0') { selection.transition().style('opacity', 1); } - } - return contributors; + return function(selection) { + update(selection); + context.connection().on('load.contributors', function() { + update(selection); + }); + + context.map().on('move.contributors', _.debounce(function() { + update(selection); + }, 500)); + }; }; diff --git a/js/id/ui/flash.js b/js/id/ui/flash.js index 1e4d7379d..416b4a144 100644 --- a/js/id/ui/flash.js +++ b/js/id/ui/flash.js @@ -1,5 +1,5 @@ -iD.ui.flash = function() { - var modal = iD.ui.modal(); +iD.ui.flash = function(selection) { + var modal = iD.ui.modal(selection); modal.select('.modal').classed('modal-flash', true); diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index ed37d2cb1..cc0ce205c 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -1,67 +1,103 @@ -iD.ui.geocoder = function() { - - var map; +iD.ui.Geocoder = function(context) { + function resultExtent(bounds) { + return new iD.geo.Extent( + [parseFloat(bounds[3]), parseFloat(bounds[0])], + [parseFloat(bounds[2]), parseFloat(bounds[1])]); + } function geocoder(selection) { + + var shown = false; + function keydown() { if (d3.event.keyCode !== 13) return; d3.event.preventDefault(); - d3.json('http://a.tiles.mapbox.com/v3/openstreetmap.map-hn253zqn/geocode/' + - encodeURIComponent(this.value) + '.json', function(err, resp) { - if (err) return hide(); - hide(); - if (!resp.results.length) { - return iD.ui.flash() - .select('.content') - .append('h3') - .text('No location found for "' + resp.query[0] + '"'); - } - var bounds = resp.results[0][0].bounds; - map.extent(iD.geo.Extent([bounds[0], bounds[1]], [bounds[2], bounds[3]])); - }); - } + var searchVal = this.value; + d3.json('http://nominatim.openstreetmap.org/search/' + + encodeURIComponent(searchVal) + '?limit=10&format=json', function(err, resp) { + if (err) return hide(); + if (!resp.length) { + return iD.ui.flash(context.container()) + .select('.content') + .append('h3') + .text(t('geocoder.no_results', {name: searchVal})); + } else if (resp.length > 1) { + var spans = resultsList.selectAll('span') + .data(resp, function(d) { return d.place_id; }); - function clickoutside(selection) { - selection - .on('click.geocoder-inside', function() { - return d3.event.stopPropagation(); + spans.enter() + .append('span') + .text(function(d) { + return d.type.charAt(0).toUpperCase() + d.type.slice(1) + ': '; + }) + .append('a') + .text(function(d) { + if (d.display_name.length > 80) { + return d.display_name.substr(0, 80) + '…'; + } else { + return d.display_name; + } + }) + .on('click', clickResult); + spans.exit().remove(); + resultsList.classed('hide', false); + } else { + applyBounds(resultExtent(resp[0].boundingbox)); + } }); - d3.select('body').on('click.geocoder-outside', hide); } - function show() { setVisible(true); } + function clickResult(d) { + applyBounds(resultExtent(d.boundingbox)); + } + + function applyBounds(extent) { + hide(); + var map = context.map(); + map.extent(extent); + if (map.zoom() > 19) map.zoom(19); + } + function hide() { setVisible(false); } function toggle() { setVisible(gcForm.classed('hide')); } function setVisible(show) { - button.classed('active', show); - gcForm.classed('hide', !show); - var input_node = d3.select('.map-overlay input').node(); - if (show) input_node.focus(); - else input_node.blur(); + if (show !== shown) { + button.classed('active', show); + gcForm.call(iD.ui.Toggle(show)); + if (!show) resultsList.classed('hide', !show); + if (show) inputNode.node().focus(); + else inputNode.node().blur(); + shown = show; + } } var button = selection.append('button') .attr('tabindex', -1) - .attr('title', 'Find A Location') - .html('') - .on('click', toggle); + .attr('title', t('geocoder.title')) + .on('click', toggle) + .call(bootstrap.tooltip() + .placement('right')); + + button.append('span') + .attr('class', 'icon geocode'); var gcForm = selection.append('form'); - gcForm.attr('class','content fillD map-overlay hide') + var inputNode = gcForm.attr('class', 'content fillD map-overlay hide') .append('input') - .attr({ type: 'text', placeholder: 'find a place' }) - .on('keydown', keydown); + .attr({ type: 'text', placeholder: t('geocoder.placeholder') }) + .on('keydown', keydown); - selection.call(clickoutside); + var resultsList = selection.append('div') + .attr('class', 'content fillD map-overlay hide'); + + selection.on('click.geocoder-inside', function() { + return d3.event.stopPropagation(); + }); + + context.container().on('click.geocoder-outside', hide); } - geocoder.map = function(_) { - if (!arguments.length) return map; - map = _; - return geocoder; - }; - return geocoder; }; diff --git a/js/id/ui/geolocate.js b/js/id/ui/geolocate.js index 14b060e8a..d1d3f3772 100644 --- a/js/id/ui/geolocate.js +++ b/js/id/ui/geolocate.js @@ -1,4 +1,8 @@ -iD.ui.geolocate = function(map) { +iD.ui.Geolocate = function(map) { + function click() { + navigator.geolocation.getCurrentPosition( + success, error); + } function success(position) { map.center([position.coords.longitude, position.coords.latitude]); @@ -7,17 +11,16 @@ iD.ui.geolocate = function(map) { function error() { } return function(selection) { - selection - .attr('class', 'geolocate-control map-control') - .append('button') - .attr('tabindex', -1) - .attr('title', 'Show My Location') - .on('click', function() { - navigator.geolocation.getCurrentPosition( - success, error); - }) - .append('span') - .attr('class','icon geolocate'); - }; + if (!navigator.geolocation) return; + var button = selection.append('button') + .attr('tabindex', -1) + .attr('title', t('geolocate.title')) + .on('click', click) + .call(bootstrap.tooltip() + .placement('right')); + + button.append('span') + .attr('class', 'icon geolocate'); + }; }; diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index d73e3c3b0..7357646d1 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -1,15 +1,15 @@ -iD.ui.inspector = function() { +iD.ui.Inspector = function() { var event = d3.dispatch('changeTags', 'close'), taginfo = iD.taginfo(), initial = false, - graph, + context, tagList; function inspector(selection) { var entity = selection.datum(); var inspector = selection.append('div') - .attr('class','inspector content'); + .attr('class','inspector content hide'); inspector.append('div') .attr('class', 'head inspector-inner fillL') @@ -22,26 +22,32 @@ iD.ui.inspector = function() { .attr('class', 'inspector-inner tag-wrap fillL2'); inspectorwrap.append('h4') - .text('Edit tags'); + .text(t('inspector.edit_tags')); tagList = inspectorwrap.append('ul'); - inspectorwrap - .append('div') - .attr('class', 'add-tag-row') - .append('button') - .attr('class', 'add-tag') - .text('+ Add New Tag') - .on('click', function() { - addTag(); - focusNewKey(); - }); + var newTag = inspectorwrap.append('button') + .attr('class', 'add-tag'); + + newTag.on('click', function () { + addTag(); + focusNewKey(); + }); + + newTag.append('span') + .attr('class', 'icon icon-pre-text plus'); + + newTag.append('span') + .attr('class', 'label') + .text(t('inspector.new_tag')); drawTags(entity.tags); inspectorbody.append('div') .attr('class', 'inspector-buttons pad1 fillD') .call(drawButtons); + + inspector.call(iD.ui.Toggle(true)); } function drawHead(selection) { @@ -50,7 +56,7 @@ iD.ui.inspector = function() { var h2 = selection.append('h2'); h2.append('span') - .attr('class', 'icon big icon-pre-text big-' + entity.geometry(graph)); + .attr('class', 'icon big icon-pre-text big-' + entity.geometry(context.graph())); h2.append('span') .text(entity.friendlyName()); @@ -60,18 +66,20 @@ iD.ui.inspector = function() { var entity = selection.datum(); var inspectorButton = selection.append('button') - .attr('class', 'apply action') - .on('click', apply); + .attr('class', 'apply action') + .on('click', apply); - inspectorButton.append('span').attr('class','icon icon-pre-text apply'); - inspectorButton.append('span').attr('class','label').text('Okay'); + inspectorButton.append('span') + .attr('class','label') + .text(t('inspector.okay')); - var minorButtons = selection.append('div').attr('class','minor-buttons fl'); + var minorButtons = selection.append('div') + .attr('class','minor-buttons fl'); - minorButtons.append('a') - .attr('href', 'http://www.openstreetmap.org/browse/' + entity.type + '/' + entity.osmId()) - .attr('target', '_blank') - .text('View on OSM'); + minorButtons.append('a') + .attr('href', 'http://www.openstreetmap.org/browse/' + entity.type + '/' + entity.osmId()) + .attr('target', '_blank') + .text(t('inspector.view_on_osm')); } function drawTags(tags) { @@ -118,58 +126,85 @@ iD.ui.inspector = function() { .on('click', removeTag); removeBtn.append('span') - .attr('class', 'icon remove'); + .attr('class', 'icon delete'); + + function findLocal(docs) { + var locale = iD.detect().locale.toLowerCase(), + localized; + + localized = _.find(docs, function(d) { + return d.lang.toLowerCase() === locale; + }); + if (localized) return localized; + + // try the non-regional version of a language, like + // 'en' if the language is 'en-US' + if (locale.indexOf('-') !== -1) { + var first = locale.split('-')[0]; + localized = _.find(docs, function(d) { + return d.lang.toLowerCase() === first; + }); + if (localized) return localized; + } + + // finally fall back to english + return _.find(docs, function(d) { + return d.lang.toLowerCase() === 'en'; + }); + } + + function keyValueReference(err, docs) { + var local; + if (!err && docs) { + local = findLocal(docs); + } + if (local) { + var types = []; + if (local.on_area) types.push('area'); + if (local.on_node) types.push('point'); + if (local.on_way) types.push('line'); + local.types = types; + iD.ui.modal(context.container()) + .select('.content') + .datum(local) + .call(iD.ui.tagReference); + } else { + iD.ui.flash(context.container()) + .select('.content') + .append('h3') + .text(t('inspector.no_documentation_combination')); + } + } + + function keyReference(err, values, params) { + if (!err && values.length) { + iD.ui.modal(context.container()) + .select('.content') + .datum({ + data: values, + title: 'Key:' + params.key, + geometry: params.geometry + }) + .call(iD.ui.keyReference); + } else { + iD.ui.flash(context.container()) + .select('.content') + .append('h3') + .text(t('inspector.no_documentation_key')); + } + } var helpBtn = row.append('button') .attr('tabindex', -1) .attr('class', 'tag-help minor') .on('click', function(d) { var params = _.extend({}, d, { - geometry: entity.geometry(graph) + geometry: entity.geometry(context.graph()) }); if (d.key && d.value) { - taginfo.docs(params, function(err, docs) { - var en; - if (!err && docs) { - en = _.find(docs, function(d) { - return d.lang == 'en'; - }); - } - if (en) { - var types = []; - if (en.on_area) types.push('area'); - if (en.on_node) types.push('point'); - if (en.on_way) types.push('line'); - en.types = types; - iD.ui.modal() - .select('.content') - .datum(en) - .call(iD.ui.tagReference); - } else { - iD.ui.flash() - .select('.content') - .append('h3') - .text('This is no documentation available for this tag combination'); - } - }); + taginfo.docs(params, keyValueReference); } else if (d.key) { - taginfo.values(params, function(err, values) { - if (!err && values.data.length) { - iD.ui.modal() - .select('.content') - .datum({ - data: values.data, - title: 'Key:' + params.key, - geometry: params.geometry - }) - .call(iD.keyReference); - } else { - iD.ui.flash() - .select('.content') - .append('h3') - .text('This is no documentation available for this key'); - } - }); + taginfo.values(params, keyReference); } }); @@ -195,7 +230,7 @@ iD.ui.inspector = function() { function bindTypeahead() { var entity = tagList.datum(), - geometry = entity.geometry(graph), + geometry = entity.geometry(context.graph()), row = d3.select(this), key = row.selectAll('.key'), value = row.selectAll('.value'); @@ -276,8 +311,8 @@ iD.ui.inspector = function() { return inspector; }; - inspector.graph = function(_) { - graph = _; + inspector.context = function(_) { + context = _; return inspector; }; diff --git a/js/id/ui/lasso.js b/js/id/ui/lasso.js new file mode 100644 index 000000000..4644364c3 --- /dev/null +++ b/js/id/ui/lasso.js @@ -0,0 +1,60 @@ +iD.ui.Lasso = function() { + + var center, box, + group, + a = [0, 0], + b = [0, 0]; + + function lasso(selection) { + + group = selection.append('g') + .attr('class', 'lasso hide'); + + box = group.append('rect') + .attr('class', 'lasso-box'); + + group.call(iD.ui.Toggle(true)); + + } + + // top-left + function topLeft(d) { + return 'translate(' + Math.min(d[0][0], d[1][0]) + ',' + Math.min(d[0][1], d[1][1]) + ')'; + } + + function width(d) { return Math.abs(d[0][0] - d[1][0]); } + function height(d) { return Math.abs(d[0][1] - d[1][1]); } + + function draw() { + if (box) { + box.data([[a, b]]) + .attr('transform', topLeft) + .attr('width', width) + .attr('height', height); + } + } + + lasso.a = function(_) { + if (!arguments.length) return a; + a = _; + draw(); + return lasso; + }; + + lasso.b = function(_) { + if (!arguments.length) return b; + b = _; + draw(); + return lasso; + }; + + lasso.close = function(selection) { + if (group) { + group.call(iD.ui.Toggle(false, function() { + d3.select(this).remove(); + })); + } + }; + + return lasso; +}; diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index 282bd75e2..da800596f 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -1,166 +1,193 @@ -iD.ui.layerswitcher = function(map) { +iD.ui.LayerSwitcher = function(context) { var event = d3.dispatch('cancel', 'save'), - sources = [{ - name: 'Bing', - source: iD.BackgroundSource.Bing, - description: 'Satellite imagery.', - link: 'http://opengeodata.org/microsoft-imagery-details' - }, { - name: 'TIGER 2012', - source: iD.BackgroundSource.Tiger2012, - description: 'Public domain road data from the US Government.' - }, { - name: 'OSM', - source: iD.BackgroundSource.OSM, - description: 'The default OpenStreetMap layer.', - link: 'http://www.openstreetmap.org/' - }, { - name: 'MapBox', - source: iD.BackgroundSource.MapBox, - description: 'Satellite and Aerial Imagery', - link: 'http://mapbox.com' - }, { - name: 'Custom', - source: iD.BackgroundSource.Custom, - description: 'A custom layer (requires configuration)' - }], opacities = [1, 0.5, 0]; + var layers = iD.layers; + + function getSources() { + var ext = context.map().extent(); + return layers.filter(function(layer) { + return !layer.data.extent || + iD.geo.Extent(layer.data.extent).intersects(ext); + }); + } + function layerswitcher(selection) { var content = selection - .append('div').attr('class', 'content fillD map-overlay hide'); + .append('div').attr('class', 'content fillD map-overlay hide'), + shown = false; var button = selection .append('button') .attr('tabindex', -1) .attr('class', 'fillD') - .attr('title', 'Layer Settings') - .html("") - .on('click.layerswitcher-toggle', toggle); + .attr('title', t('layerswitcher.description')) + .on('click.layerswitcher-toggle', toggle) + .call(bootstrap.tooltip() + .placement('right')); + + button.append('span') + .attr('class', 'layers icon'); - function show() { setVisible(true); } function hide() { setVisible(false); } function toggle() { setVisible(content.classed('hide')); } function setVisible(show) { - button.classed('active', show); - content.classed('hide', !show); + if (show !== shown) { + button.classed('active', show); + content.call(iD.ui.Toggle(show)); + shown = show; + } } function clickoutside(selection) { selection.on('click.layerswitcher-inside', function() { return d3.event.stopPropagation(); }); - d3.select('body').on('click.layerswitcher-outside', hide); + context.container().on('click.layerswitcher-outside', hide); } var opa = content .append('div') .attr('class', 'opacity-options-wrapper'); - opa.append('h4').text('Layers'); + opa.append('h4').text(t('layerswitcher.title')); - opa.append('ul') - .attr('class', 'opacity-options') - .selectAll('div.opacity') + var opacityList = opa.append('ul') + .attr('class', 'opacity-options'); + + function setOpacity(d) { + context.map().tilesurface + .transition() + .style('opacity', d) + .attr('data-opacity', d); + opacityList.selectAll('li') + .classed('selected', false); + d3.select(this) + .classed('selected', true); + } + + opacityList.selectAll('div.opacity') .data(opacities) .enter() .append('li') .attr('data-original-title', function(d) { - return (d * 100) + "% opacity"; - }) - .on('click.set-opacity', function(d) { - d3.select('#tile-g') - .transition() - .style('opacity', d) - .attr('data-opacity', d); - d3.selectAll('.opacity-options li') - .classed('selected', false); - d3.select(this) - .classed('selected', true); + return t('layerswitcher.percent_brightness', { opacity: (d * 100) }); }) + .on('click.set-opacity', setOpacity) .html("
") .call(bootstrap.tooltip().placement('top')) .append('div') .attr('class', 'opacity') - .style('opacity', function(d) { - return d; - }); + .style('opacity', String); // Make sure there is an active selection by default - d3.select('.opacity-options li:nth-child(2)').classed('selected', true); + opa.select('.opacity-options li:nth-child(2)').classed('selected', true); function selectLayer(d) { + content.selectAll('a.layer') .classed('selected', function(d) { - return d.source === map.background.source(); + return d === context.background().source(); }); - d3.select('#attribution a') - .attr('href', d.link) - .text('provided by ' + d.name); + + var provided_by = context.container().select('.attribution .provided-by') + .html(''); + + if (d.data.terms_url) { + provided_by.append('a') + .attr('href', (d.data.terms_url || '')) + .classed('disabled', !d.data.terms_url) + .text(' provided by ' + (d.data.sourcetag || d.data.name)); + } else { + provided_by + .text(' provided by ' + (d.data.sourcetag || d.data.name)); + } + } - content + function clickSetSource(d) { + d3.event.preventDefault(); + if (d.data.name === 'Custom') { + var configured = d(); + if (!configured) return; + d = configured; + } + context.background().source(d); + if (d.data.name === 'Custom (customized)') { + context.history() + .imagery_used('Custom (' + d.data.template + ')'); + } else { + context.history() + .imagery_used(d.data.sourcetag || d.data.name); + } + context.redraw(); + selectLayer(d); + } + + var layerList = content .append('ul') - .attr('class', 'toggle-list fillL') - .selectAll('a.layer') - .data(sources) - .enter() + .attr('class', 'toggle-list fillL'); + + function update() { + var layerLinks = layerList.selectAll('a.layer') + .data(getSources(), function(d) { + return d.data.name; + }); + layerLinks.exit().remove(); + var LayerInner = layerLinks.enter() .append('li') .append('a') .attr('data-original-title', function(d) { - return d.description; + return d.data.description || ''; }) .attr('href', '#') .attr('class', 'layer') - .text(function(d) { - return d.name; - }) - .call(bootstrap.tooltip().placement('right')) - .on('click.set-source', function(d) { - d3.event.preventDefault(); - if (d.name === 'Custom') { - var configured = d.source(); - if (!configured) return; - d.source = configured; - d.name = 'Custom (configured)'; + .each(function(d) { + // only set tooltips for layers with tooltips + if (d.data.description) { + d3.select(this).call(bootstrap.tooltip().placement('right')); } - map.background.source(d.source); - map.history().imagery_used(d.name); - map.redraw(); - selectLayer(d); }) - .insert('span') + .on('click.set-source', clickSetSource); + LayerInner.insert('span') .attr('class','icon toggle'); + LayerInner.insert('span').text(function(d) { + return d.data.name; + }); + + selectLayer(context.background().source()); + } + + context.map().on('move.layerswitcher-update', _.debounce(update, 1000)); var adjustments = content .append('div') .attr('class', 'adjustments pad1'); var directions = [ - ['←', [-1, 0]], - ['↑', [0, -1]], - ['→', [1, 0]], - ['↓', [0, 1]]]; + ['left', [-1, 0]], + ['top', [0, -1]], + ['right', [1, 0]], + ['bottom', [0, 1]]]; function nudge(d) { - map.background.nudge(d[1]); - map.redraw(); + context.background().nudge(d[1]); + context.redraw(); } adjustments.append('a') - .text('▶ fix misalignment') + .text(t('layerswitcher.fix_misalignment')) .attr('href', '#') + .classed('alignment-toggle', true) .classed('expanded', false) .on('click', function() { var exp = d3.select(this).classed('expanded'); if (!exp) { nudge_container.style('display', 'block'); - d3.select(this).text('▼ fix misalignment'); } else { nudge_container.style('display', 'none'); - d3.select(this).text('▶ fix misalignment'); } d3.select(this).classed('expanded', !exp); d3.event.preventDefault(); @@ -174,20 +201,20 @@ iD.ui.layerswitcher = function(map) { nudge_container.selectAll('button') .data(directions).enter() .append('button') - .attr('class', 'nudge') + .attr('class', function(d) { return d[0] + ' nudge'; }) .text(function(d) { return d[0]; }) .on('click', nudge); nudge_container.append('button') - .text('reset') + .text(t('layerswitcher.reset')) .attr('class', 'reset') .on('click', function() { - map.background.offset([0, 0]); - map.redraw(); + context.background().offset([0, 0]); + context.redraw(); }); selection.call(clickoutside); - selectLayer(map.background.source()); + selectLayer(context.background().source()); } return d3.rebind(layerswitcher, event, 'on'); diff --git a/js/id/ui/loading.js b/js/id/ui/loading.js index 23d5b7c2d..c07426a0b 100644 --- a/js/id/ui/loading.js +++ b/js/id/ui/loading.js @@ -1,11 +1,12 @@ -iD.ui.loading = function(message, blocking) { - var modal = iD.ui.modal(blocking); +iD.ui.loading = function(selection, message, blocking) { + var modal = iD.ui.modal(selection, blocking); var loadertext = modal.select('.content') .classed('loading-modal', true) .append('div').attr('class','modal-section fillL'); - loadertext.append('img').attr('class','loader').attr('src', '/img/loader.gif'); - loadertext.append('h3').text(message || ''); + + loadertext.append('img').attr('class','loader').attr('src', 'img/loader.gif'); + loadertext.append('h3').text(message || ''); return modal; }; diff --git a/js/id/ui/modal.js b/js/id/ui/modal.js index b91c2772a..cd69e0219 100644 --- a/js/id/ui/modal.js +++ b/js/id/ui/modal.js @@ -1,6 +1,7 @@ -iD.ui.modal = function(blocking) { +iD.ui.modal = function(selection, blocking) { - var animate = d3.select('div.modal').empty(); + var previous = selection.select('div.modal'); + var animate = previous.empty(); var keybinding = d3.keybinding('modal') .on('⌫', close) @@ -8,10 +9,10 @@ iD.ui.modal = function(blocking) { d3.select(document).call(keybinding); - d3.select('div.modal').transition() + previous.transition() .style('opacity', 0).remove(); - var shaded = d3.select(document.body) + var shaded = selection .append('div') .attr('class', 'shaded') .style('opacity', 0) diff --git a/js/id/ui/modes.js b/js/id/ui/modes.js new file mode 100644 index 000000000..d3db0074a --- /dev/null +++ b/js/id/ui/modes.js @@ -0,0 +1,68 @@ +iD.ui.Modes = function(context) { + var modes = [ + iD.modes.Browse(context), + iD.modes.AddPoint(context), + iD.modes.AddLine(context), + iD.modes.AddArea(context)]; + + return function(selection, limiter) { + var buttons = selection.selectAll('button.add-button') + .data(modes); + + buttons.enter().append('button') + .attr('tabindex', -1) + .attr('class', function(mode) { return mode.id + ' add-button col3'; }) + .on('click.mode-buttons', function(mode) { context.enter(mode); }) + .call(bootstrap.tooltip() + .placement('bottom') + .html(true) + .title(function(mode) { + return iD.ui.tooltipHtml(mode.description, mode.key); + })); + + var notice = iD.ui.notice(limiter) + .message(false) + .on('zoom', function() { context.map().zoom(16); }); + + function disableTooHigh() { + if (context.map().editable()) { + notice.message(false); + buttons.attr('disabled', null); + } else { + buttons.attr('disabled', 'disabled'); + notice.message(true); + context.enter(iD.modes.Browse(context)); + } + } + + context.map() + .on('move.mode-buttons', _.debounce(disableTooHigh, 500)); + + buttons.append('span') + .attr('class', function(mode) { return mode.id + ' icon icon-pre-text'; }); + + buttons.append('span') + .attr('class', 'label') + .text(function(mode) { return mode.title; }); + + context.on('enter.editor', function(entered) { + buttons.classed('active', function(mode) { return entered.button === mode.button; }); + context.container() + .classed("mode-" + entered.id, true); + }); + + context.on('exit.editor', function(exited) { + context.container() + .classed("mode-" + exited.id, false); + }); + + var keybinding = d3.keybinding('mode-buttons'); + + modes.forEach(function(m) { + keybinding.on(m.key, function() { if (context.map().editable()) context.enter(m); }); + }); + + d3.select(document) + .call(keybinding); + }; +}; diff --git a/js/id/ui/notice.js b/js/id/ui/notice.js index 479673ba1..12b1a2c09 100644 --- a/js/id/ui/notice.js +++ b/js/id/ui/notice.js @@ -4,21 +4,18 @@ iD.ui.notice = function(selection) { notice = {}; var div = selection.append('div') - .attr('class', 'notice') - .append('div') - .attr('class', 'notice-inner'); + .attr('class', 'notice'); - div.append('button') - .attr('class', 'zoom-to') - .on('click', function() { - event.zoom(); - }) - .append('span') - .attr('class', 'icon invert zoom-in'); + var button = div.append('button') + .attr('class', 'zoom-to notice') + .on('click', event.zoom); - div.append('span') - .attr('class', 'notice-text') - .text('zoom in to edit the map'); + button.append('span') + .attr('class', 'icon zoom-in-invert'); + + button.append('span') + .attr('class', 'label') + .text(t('zoom_in_edit')); notice.message = function(_) { if (_) { diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index d38ef3679..e718cfe51 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -1,54 +1,105 @@ -iD.ui.RadialMenu = function(entity, mode) { - var arcs; +iD.ui.RadialMenu = function(operations) { + var menu, + center = [0, 0]; - var radialMenu = function(selection, center) { - var history = mode.map.history(), - graph = history.graph(), - operations = d3.values(iD.operations) - .map(function (o) { return o(entity.id, mode); }) - .filter(function (o) { return o.available(graph); }); + var radialMenu = function(selection) { + if (!operations.length) + return; function click(operation) { d3.event.stopPropagation(); - operation(history); + operation(); } - var arc = d3.svg.arc() - .outerRadius(70) - .innerRadius(30) - .startAngle(function (d, i) { return 2 * Math.PI / operations.length * i; }) - .endAngle(function (d, i) { return 2 * Math.PI / operations.length * (i + 1); }); - - arcs = selection.selectAll() - .data(operations) - .enter().append('g') + menu = selection.append('g') .attr('class', 'radial-menu') .attr('transform', "translate(" + center + ")") .attr('opacity', 0); - arcs.transition() + menu.transition() .attr('opacity', 0.8); - arcs.append('path') - .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) - .attr('d', arc) - .classed('disabled', function (d) { return !d.enabled(graph); }) - .on('click', click); + var r = 50, + a = Math.PI / 4, + a0 = -Math.PI / 4, + a1 = a0 + (operations.length - 1) * a; - arcs.append('text') - .attr("transform", function(d, i) { return "translate(" + arc.centroid(d, i) + ")"; }) - .attr("dy", ".35em") - .style("text-anchor", "middle") - .text(function(d) { return d.title; }); + menu.append('path') + .attr('class', 'radial-menu-background') + .attr('d', 'M' + r * Math.sin(a0) + ',' + + r * Math.cos(a0) + + ' A' + r + ',' + r + ' 0 0,0 ' + + r * Math.sin(a1) + ',' + + r * Math.cos(a1)) + .attr('stroke-width', 50) + .attr('stroke-linecap', 'round'); + + var button = menu.selectAll() + .data(operations) + .enter().append('g') + .attr('transform', function(d, i) { + return 'translate(' + r * Math.sin(a0 + i * a) + ',' + + r * Math.cos(a0 + i * a) + ')'; + }); + + button.append('circle') + .attr('class', function(d) { return 'radial-menu-item radial-menu-item-' + d.id; }) + .attr('r', 15) + .attr('title', function(d) { return d.title; }) + .classed('disabled', function(d) { return !d.enabled(); }) + .on('click', click) + .on('mouseover', mouseover) + .on('mouseout', mouseout); + + var image = button.append('foreignObject') + .style('pointer-events', 'none') + .attr('width', 20) + .attr('height', 20) + .attr('x', -10) + .attr('y', -10); + + image.append('xhtml:span') + .attr('class', function(d) { return 'icon icon-operation icon-operation-' + d.id; }); + + var tooltip = menu.append('foreignObject') + .style('display', 'none') + .attr('width', 200) + .attr('height', 400); + + tooltip.append('xhtml:div') + .attr('class', 'radial-menu-tooltip'); + + function mouseover(d, i) { + var angle = a0 + i * a, + dx = angle < 0 ? -200 : 0, + dy = 0; + + tooltip + .attr('x', (r + 30) * Math.sin(angle) + dx) + .attr('y', (r + 30) * Math.cos(angle) + dy) + .style('display', 'block') + .select('div') + .text(d.description); + } + + function mouseout() { + tooltip.style('display', 'none'); + } }; radialMenu.close = function(selection) { - if (arcs) { - arcs.transition() + if (menu) { + menu.transition() .attr('opacity', 0) .remove(); } }; + radialMenu.center = function(_) { + if (!arguments.length) return center; + center = _; + return radialMenu; + }; + return radialMenu; }; diff --git a/js/id/ui/restore.js b/js/id/ui/restore.js new file mode 100644 index 000000000..c6996ff23 --- /dev/null +++ b/js/id/ui/restore.js @@ -0,0 +1,41 @@ +iD.ui.Restore = function(context) { + return function(selection) { + if (!context.history().lock() || !context.history().restorableChanges()) + return; + + var modal = iD.ui.modal(selection); + + modal.select('.modal') + .attr('class', 'modal-splash modal'); + + var introModal = modal.select('.content'); + + introModal.append('div') + .attr('class', 'modal-section fillL') + .append('h3') + .text(t('restore.description')); + + var buttonWrap = introModal.append('div') + .attr('class', 'modal-section fillD cf col12'); + + var buttons = buttonWrap + .append('div') + .attr('class', 'button-wrap joined col6'); + + buttons.append('button') + .attr('class', 'save action button col6') + .text(t('restore.restore')) + .on('click', function() { + context.history().load(); + modal.remove(); + }); + + buttons.append('button') + .attr('class', 'cancel button col6') + .text(t('restore.reset')) + .on('click', function() { + context.history().clearSaved(); + modal.remove(); + }); + }; +}; diff --git a/js/id/ui/save.js b/js/id/ui/save.js index 50f9891f7..5e05183b2 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -1,106 +1,110 @@ -iD.ui.save = function() { +iD.ui.Save = function(context) { + var map = context.map(), + history = context.history(), + connection = context.connection(), + key = iD.ui.cmd('⌘S'), + modal; - var map, controller; + function save() { + d3.event.preventDefault(); - function save(selection) { + if (!history.hasChanges()) return; - var history = map.history(), - connection = map.connection(), - tooltip = bootstrap.tooltip() - .placement('bottom'); - - selection.html("Save") - .attr('title', 'Save changes to OpenStreetMap, making them visible to other users') - .attr('tabindex', -1) - .property('disabled', true) - .call(tooltip) - .on('click', function() { - - function commit(e) { - d3.select('.shaded').remove(); - var l = iD.ui.loading('Uploading changes to OpenStreetMap.', true); - connection.putChangeset(history.changes(), e.comment, history.imagery_used(), function(err, changeset_id) { - l.remove(); - history.reset(); - map.flush().redraw(); - if (err) { - var desc = iD.ui.confirm() - .select('.description'); - desc.append('h2') - .text('An error occurred while trying to save'); - desc.append('p').text(err.responseText); - } else { - var modal = iD.ui.modal(); - modal.select('.content') - .classed('success-modal', true) - .datum({ - id: changeset_id, - comment: e.comment - }) - .call(iD.ui.success(connection) - .on('cancel', function() { - modal.remove(); - })); - } - }); - } - - if (history.hasChanges()) { - connection.authenticate(function(err) { - var modal = iD.ui.modal(); - var changes = history.changes(); - changes.connection = connection; - modal.select('.content') - .classed('commit-modal', true) - .datum(changes) - .call(iD.ui.commit(map) - .on('cancel', function() { - modal.remove(); - }) - .on('fix', function(d) { - map.extent(d.entity.extent(map.history().graph())); - if (map.zoom() > 19) map.zoom(19); - controller.enter(iD.modes.Select(d.entity)); - modal.remove(); - }) - .on('save', commit)); - }); - } else { - iD.ui.confirm().select('.description') - .append('h3').text('You don\'t have any changes to save.'); - } - - }); - - selection.append('span') - .attr('class', 'count'); - - history.on('change.save-button', function() { - var hasChanges = history.hasChanges(); - - selection - .property('disabled', !hasChanges) - .classed('has-count', hasChanges) - .select('span.count') - .text(history.numChanges()); - - if (!hasChanges) { - selection.call(tooltip.hide); - } + connection.authenticate(function(err) { + modal = iD.ui.modal(context.container()); + var changes = history.changes(); + changes.connection = connection; + modal.select('.content') + .classed('commit-modal', true) + .datum(changes) + .call(iD.ui.Commit(context) + .on('cancel', function() { + modal.remove(); + }) + .on('fix', clickFix) + .on('save', commit)); }); } - save.map = function(_) { - if (!arguments.length) return map; - map = _; - return save; - }; + function commit(e) { + context.container().select('.shaded') + .remove(); - save.controller = function(_) { - if (!arguments.length) return controller; - controller = _; - return save; - }; + var loading = iD.ui.loading(context.container(), t('save.uploading'), true); - return save; + connection.putChangeset( + history.changes(), + e.comment, + history.imagery_used(), + function(err, changeset_id) { + loading.remove(); + history.reset(); + map.flush().redraw(); + if (err) { + var desc = iD.ui.confirm() + .select('.description'); + desc.append('h2') + .text(t('save.error')); + desc.append('p').text(err.responseText); + } else { + success(e, changeset_id); + } + }); + } + + function success(e, changeset_id) { + modal = iD.ui.modal(context.container()); + modal.select('.content') + .classed('success-modal', true) + .datum({ + id: changeset_id, + comment: e.comment + }) + .call(iD.ui.Success(connection) + .on('cancel', function() { + modal.remove(); + })); + } + + function clickFix(d) { + var extent = d.entity.extent(context.graph()); + map.centerZoom(extent.center(), Math.min(19, map.extentZoom(extent))); + context.enter(iD.modes.Select(context, [d.entity.id])); + modal.remove(); + } + + return function(selection) { + var button = selection.append('button') + .attr('class', 'save col12 disabled') + .attr('tabindex', -1) + .on('click', save) + .call(bootstrap.tooltip() + .placement('bottom') + .html(true) + .title(iD.ui.tooltipHtml(t('save.help'), key))); + + button.append('span') + .attr('class', 'label') + .text(t('save.title')); + + button.append('span') + .attr('class', 'count'); + + var keybinding = d3.keybinding('undo-redo') + .on(key, save); + + d3.select(document) + .call(keybinding); + + context.history().on('change.save', function() { + var hasChanges = history.hasChanges(); + + button + .classed('disabled', !hasChanges) + .classed('has-count', hasChanges); + + button.select('span.count') + .text(history.numChanges()); + }); + }; }; diff --git a/js/id/ui/source_switch.js b/js/id/ui/source_switch.js new file mode 100644 index 000000000..624e26af4 --- /dev/null +++ b/js/id/ui/source_switch.js @@ -0,0 +1,25 @@ +iD.ui.SourceSwitch = function(context) { + function click() { + d3.event.preventDefault(); + + var live = d3.select(this).classed('live'); + + context.map() + .flush(); + + context.connection() + .url(live ? 'http://api06.dev.openstreetmap.org' : 'http://www.openstreetmap.org'); + + d3.select(this) + .text(live ? t('source_switch.dev') : t('source_switch.live')) + .classed('live', !live); + } + + return function(selection) { + selection.append('a') + .attr('href', '#') + .text(t('source_switch.live')) + .classed('live', true) + .on('click', click); + }; +}; diff --git a/js/id/ui/splash.js b/js/id/ui/splash.js index 43489fab1..c0e79261c 100644 --- a/js/id/ui/splash.js +++ b/js/id/ui/splash.js @@ -1,16 +1,32 @@ -iD.ui.splash = function() { - var modal = iD.ui.modal(); +iD.ui.Splash = function(context) { + return function(selection) { + if (context.storage('sawSplash')) + return; - modal.select('.modal') - .attr('class', 'modal-splash modal') + context.storage('sawSplash', true); - var introModal = modal.select('.content') - .append('div') - .attr('class', 'modal-section fillL'); + var modal = iD.ui.modal(selection); - introModal.append('div').attr('class','logo'); + modal.select('.modal') + .attr('class', 'modal-splash modal'); - introModal.append('div').html("

Welcome to the iD OpenStreetMap editor

This is development version 0.0.0-alpha1. For more information see ideditor.com and report bugs at github.com.systemed/iD.

"); + var introModal = modal.select('.content') + .append('div') + .attr('class', 'modal-section fillL'); - return modal; -}; \ No newline at end of file + introModal.append('div') + .attr('class', 'logo'); + + var div = introModal.append('div'); + + div.append("h2") + .text(t('splash.welcome')); + + div.append("p") + .html(t('splash.text', { + version: iD.version, + website: 'ideditor.com', + github: 'github.com' + })); + } +}; diff --git a/js/id/ui/success.js b/js/id/ui/success.js index 8cc6e328b..17a577fb3 100644 --- a/js/id/ui/success.js +++ b/js/id/ui/success.js @@ -1,4 +1,4 @@ -iD.ui.success = function(connection) { +iD.ui.Success = function(connection) { var event = d3.dispatch('cancel', 'save'); function success(selection) { @@ -8,7 +8,7 @@ iD.ui.success = function(connection) { var section = body.append('div').attr('class','modal-section fillD'); - header.append('h2').text('You Just Edited OpenStreetMap!'); + header.append('h2').text(t('just_edited')); var m = ''; if (changeset.comment) { @@ -24,7 +24,7 @@ iD.ui.success = function(connection) { }) .attr('target', '_blank') .attr('class', 'success-action') - .text('View on OSM'); + .text(t('view_on_osm')); header.append('a') .attr('target', '_blank') diff --git a/js/id/ui/tag_reference.js b/js/id/ui/tag_reference.js index eb1ee84bd..8a8ea2041 100644 --- a/js/id/ui/tag_reference.js +++ b/js/id/ui/tag_reference.js @@ -11,7 +11,7 @@ iD.ui.tagReference = function(selection) { .enter() .append('span') .attr('title', function(d) { - return 'used with ' + d; + return t('tag_reference.used_with', {type: d}); }) .attr('class', function(d) { return 'icon big icon-pre-text big-' + d; @@ -19,12 +19,12 @@ iD.ui.tagReference = function(selection) { header.append('span') .text(g('title')); - referenceBody = selection.append('div') + var referenceBody = selection.append('div') .attr('class','modal-section fillL2'); referenceBody .append('h5') - .text('Description'); + .text(t('tag_reference.description')); if (selection.datum().image) { referenceBody @@ -36,6 +36,7 @@ iD.ui.tagReference = function(selection) { referenceBody .append('p') .text(g('description')); + referenceBody .append('a') .attr('target', '_blank') @@ -43,7 +44,7 @@ iD.ui.tagReference = function(selection) { return 'http://wiki.openstreetmap.org/wiki/' + d.title; }) .text(function(d) { - return d.title + ' on wiki.osm.org'; + return t('tag_reference.on_wiki', {tag: d.title}); }); }); }; diff --git a/js/id/ui/toggle.js b/js/id/ui/toggle.js new file mode 100644 index 000000000..c3c299905 --- /dev/null +++ b/js/id/ui/toggle.js @@ -0,0 +1,16 @@ +// toggles the visibility of ui elements, using a combination of the +// hide class, which sets display=none, and a d3 transition for opacity. +// this will cause blinking when called repeatedly, so check that the +// value actually changes between calls. +iD.ui.Toggle = function(show, callback) { + return function(selection) { + selection.style('opacity', show ? 0 : 1) + .classed('hide', false) + .transition() + .style('opacity', show ? 1 : 0) + .each('end', function() { + d3.select(this).classed('hide', !show); + if (callback) callback.apply(this); + }); + }; +}; diff --git a/js/id/ui/undo_redo.js b/js/id/ui/undo_redo.js new file mode 100644 index 000000000..9eef75364 --- /dev/null +++ b/js/id/ui/undo_redo.js @@ -0,0 +1,47 @@ +iD.ui.UndoRedo = function(context) { + return function(selection) { + var tooltip = bootstrap.tooltip() + .placement('bottom') + .html(true); + + var undoButton = selection.append('button') + .attr('class', 'col6 disabled') + .html('') + .on('click', context.undo) + .call(tooltip); + + var redoButton = selection.append('button') + .attr('class', 'col6 disabled') + .html('') + .on('click', context.redo) + .call(tooltip); + + var keybinding = d3.keybinding('undo') + .on(iD.ui.cmd('⌘Z'), context.undo) + .on(iD.ui.cmd('⌘⇧Z'), context.redo); + + d3.select(document) + .call(keybinding); + + context.history().on('change.editor', function() { + var undo = context.history().undoAnnotation(), + redo = context.history().redoAnnotation(); + + function refreshTooltip(selection) { + if (selection.property('tooltipVisible')) { + selection.call(tooltip.show); + } + } + + undoButton + .classed('disabled', !undo) + .attr('data-original-title', iD.ui.tooltipHtml(undo || t('nothing_to_undo'), iD.ui.cmd('⌘Z'))) + .call(refreshTooltip); + + redoButton + .classed('disabled', !redo) + .attr('data-original-title', iD.ui.tooltipHtml(redo || t('nothing_to_redo'), iD.ui.cmd('⌘⇧Z'))) + .call(refreshTooltip); + }); + }; +}; diff --git a/js/id/ui/userpanel.js b/js/id/ui/userpanel.js deleted file mode 100644 index d0d8fa63d..000000000 --- a/js/id/ui/userpanel.js +++ /dev/null @@ -1,52 +0,0 @@ -iD.ui.userpanel = function(connection) { - var event = d3.dispatch('logout', 'login'); - - function user(selection) { - function update() { - if (connection.authenticated()) { - selection.style('display', 'block'); - connection.userDetails(function(user_details) { - - selection.html(''); - - // Link - var userLink = selection.append('a') - .attr('href', connection.url() + '/user/' + - user_details.display_name) - .attr('target', '_blank'); - - // Add thumbnail or dont - if (user_details.image_url) { - userLink.append('img') - .attr('class', 'icon icon-pre-text user-icon') - .attr('src', user_details.image_url); - } else { - userLink.append('span') - .attr('class','icon avatar icon-pre-text'); - } - - // Add user name - userLink.append('span') - .attr('class','label') - .text(user_details.display_name); - - selection - .append('a') - .attr('class', 'logout') - .attr('href', '#') - .text('logout') - .on('click.logout', function() { - d3.event.preventDefault(); - event.logout(); - }); - }); - } else { - selection.html('').style('display', 'none'); - } - } - connection.on('auth', update); - update(); - } - - return d3.rebind(user, event, 'on'); -}; diff --git a/js/id/ui/zoom.js b/js/id/ui/zoom.js new file mode 100644 index 000000000..823bc120f --- /dev/null +++ b/js/id/ui/zoom.js @@ -0,0 +1,40 @@ +iD.ui.Zoom = function(context) { + var zooms = [{ + id: 'zoom-in', + title: t('zoom.in'), + action: context.zoomIn, + key: '+' + }, { + id: 'zoom-out', + title: t('zoom.out'), + action: context.zoomOut, + key: '-' + }]; + + return function(selection) { + var button = selection.selectAll('button') + .data(zooms) + .enter().append('button') + .attr('tabindex', -1) + .attr('class', function(d) { return d.id; }) + .on('click.editor', function(d) { d.action(); }) + .call(bootstrap.tooltip() + .placement('right') + .html(true) + .title(function(d) { + return iD.ui.tooltipHtml(d.title, d.key); + })); + + button.append('span') + .attr('class', function(d) { return d.id + ' icon'; }); + + var keybinding = d3.keybinding('zoom') + .on('+', function() { context.zoomIn(); }) + .on('-', function() { context.zoomOut(); }) + .on('⇧=', function() { context.zoomIn(); }) + .on('dash', function() { context.zoomOut(); }); + + d3.select(document) + .call(keybinding); + } +}; diff --git a/js/id/util.js b/js/id/util.js index da31a162b..cfcf453a9 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -8,8 +8,8 @@ iD.util.trueObj = function(arr) { iD.util.tagText = function(entity) { return d3.entries(entity.tags).map(function(e) { - return e.key + ': ' + e.value; - }).join('\n'); + return e.key + '=' + e.value; + }).join(', '); }; iD.util.stringQs = function(str) { @@ -74,3 +74,21 @@ iD.util.getStyle = function(selector) { } } }; + +// a d3.mouse-alike which +// 1. Only works on HTML elements, not SVG +// 2. Does not cause style recalculation +iD.util.fastMouse = function(container) { + var rect = _.clone(container.getBoundingClientRect()), + rectLeft = rect.left, + rectTop = rect.top, + clientLeft = +container.clientLeft, + clientTop = +container.clientTop; + return function(e) { + return [ + e.clientX - rectLeft - clientLeft, + e.clientY - rectTop - clientTop]; + }; +}; + +iD.util.getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; }; diff --git a/js/id/validate.js b/js/id/validate.js new file mode 100644 index 000000000..dacff7889 --- /dev/null +++ b/js/id/validate.js @@ -0,0 +1,53 @@ +iD.validate = function(changes, graph) { + var warnings = [], change; + + // https://github.com/openstreetmap/josm/blob/mirror/src/org/ + // openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80 + function tagSuggestsArea(change) { + if (_.isEmpty(change.tags)) return false; + var tags = change.tags; + var presence = ['landuse', 'amenities', 'tourism', 'shop']; + for (var i = 0; i < presence.length; i++) { + if (tags[presence[i]] !== undefined) { + return presence[i] + '=' + tags[presence[i]]; + } + } + if (tags.building && tags.building === 'yes') return 'building=yes'; + } + + for (var i = 0; i < changes.created.length; i++) { + change = changes.created[i]; + + if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) { + warnings.push({ + message: t('validations.untagged_point'), + entity: change + }); + } + + if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) { + warnings.push({ message: t('validations.untagged_line'), entity: change }); + } + + var deprecatedTags = change.deprecatedTags(); + if (!_.isEmpty(deprecatedTags)) { + warnings.push({ + message: t('validations.deprecated_tags', { + tags: iD.util.tagText({ tags: deprecatedTags }) + }), entity: change }); + } + + if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) { + warnings.push({ message: t('validations.untagged_area'), entity: change }); + } + + if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) { + warnings.push({ + message: t('validations.tag_suggests_area', {tag: tagSuggestsArea(change)}), + entity: change + }); + } + } + + return warnings.length ? [warnings] : []; +}; diff --git a/js/lib/d3.combobox.js b/js/lib/d3.combobox.js new file mode 100644 index 000000000..165456563 --- /dev/null +++ b/js/lib/d3.combobox.js @@ -0,0 +1,147 @@ +d3.combobox = function() { + var event = d3.dispatch('accept'), + autohighlight = false, + autofilter = false, + input, + container, + data; + + var typeahead = function(selection) { + var hidden, idx = autohighlight ? 0 : -1; + + var rect = selection.select('input').node().getBoundingClientRect(); + + input = selection.select('input'); + + container = selection + .insert('div', ':first-child') + .attr('class', 'combobox') + .style({ + position: 'absolute', + display: 'none', + left: '0px', + width: rect.width + 'px', + top: rect.height + 'px' + }); + + carat = selection + .insert('div', ':first-child') + .attr('class', 'combobox-carat') + .text('+') + .style({ + position: 'absolute', + left: (rect.width - 20) + 'px', + top: '0px' + }) + .on('click', function() { + update(); + show(); + }); + + selection + .on('keyup.typeahead', key); + + hidden = false; + + function hide() { + idx = autohighlight ? 0 : -1; + hidden = true; + } + + function show() { + container.style('display', 'block'); + } + + function slowHide() { + if (autohighlight && container.select('a.selected').node()) { + select(container.select('a.selected').datum()); + event.accept(); + } + window.setTimeout(hide, 150); + } + + selection + .on('focus.typeahead', show) + .on('blur.typeahead', slowHide); + + function key() { + var len = container.selectAll('a').data().length; + if (d3.event.keyCode === 40) { + idx = Math.min(idx + 1, len - 1); + return highlight(); + } else if (d3.event.keyCode === 38) { + idx = Math.max(idx - 1, 0); + return highlight(); + } else if (d3.event.keyCode === 13) { + if (container.select('a.selected').node()) { + select(container.select('a.selected').datum()); + } + event.accept(); + hide(); + } else { + update(); + } + } + + function highlight() { + container + .selectAll('a') + .classed('selected', function(d, i) { return i == idx; }); + } + + function update() { + + function run(data) { + container.style('display', function() { + return data.length ? 'block' : 'none'; + }); + + var options = container + .selectAll('a') + .data(data, function(d) { return d.value; }); + + options.enter() + .append('a') + .text(function(d) { return d.value; }) + .attr('title', function(d) { return d.title; }) + .on('click', select); + + options.exit().remove(); + + options + .classed('selected', function(d, i) { return i == idx; }); + } + + if (typeof data === 'function') data(selection, run); + else run(data); + } + + function select(d) { + input + .property('value', d.value) + .trigger('change'); + container.style('display', 'none'); + } + + }; + + typeahead.data = function(_) { + if (!arguments.length) return data; + data = _; + return typeahead; + }; + + typeahead.autofilter = function(_) { + if (!arguments.length) return autofilter; + autofilter = _; + return typeahead; + }; + + typeahead.autohighlight = function(_) { + if (!arguments.length) return autohighlight; + autohighlight = _; + return typeahead; + }; + + return d3.rebind(typeahead, event, 'on'); +}; diff --git a/js/lib/d3.keybinding.js b/js/lib/d3.keybinding.js index e16ade725..6aa39fb39 100644 --- a/js/lib/d3.keybinding.js +++ b/js/lib/d3.keybinding.js @@ -63,7 +63,7 @@ d3.keybinding = function(namespace) { callback: callback }; - code = code.toLowerCase().match(/(?:(?:[^+])+|\+\+|^\+$)/g); + code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); for (var i = 0; i < code.length; i++) { // Normalise matching errors @@ -151,7 +151,7 @@ d3.keybinding = function(namespace) { '=': 187, 'equals': 187, // Comma, or , ',': 188, comma: 188, - //'-': 189, //??? + 'dash': 189, //??? // Period, or ., or full-stop '.': 190, period: 190, 'full-stop': 190, // Slash, or /, or forward-slash diff --git a/js/lib/d3.tail.js b/js/lib/d3.tail.js index 0667227ee..4afaf97ff 100644 --- a/js/lib/d3.tail.js +++ b/js/lib/d3.tail.js @@ -32,7 +32,7 @@ d3.tail = function() { function mousemove() { if (text === false) return; var xoffset = ((d3.event.x + tooltip_size[0] + xmargin) > selection_size[0]) ? - -tooltip_size[0] - xmargin : xoffset = xmargin; + -tooltip_size[0] - xmargin : xmargin; container.style(transformProp, 'translate(' + (~~d3.event.x + xoffset) + 'px,' + ~~d3.event.y + 'px)'); diff --git a/js/lib/d3.v3.js b/js/lib/d3.v3.js index 93b676f46..818bab9dc 100644 --- a/js/lib/d3.v3.js +++ b/js/lib/d3.v3.js @@ -12,12 +12,9 @@ }; } d3 = { - version: "3.0.0pre" + version: "3.0.5" }; - var π = Math.PI, ε = 1e-6, εε = .001, d3_radians = π / 180, d3_degrees = 180 / π; - function d3_zero() { - return 0; - } + var π = Math.PI, ε = 1e-6, d3_radians = π / 180, d3_degrees = 180 / π; function d3_target(d) { return d.target; } @@ -234,7 +231,7 @@ return s; }; d3.quantile = function(values, p) { - var H = (values.length - 1) * p + 1, h = Math.floor(H), v = values[h - 1], e = H - h; + var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; return e ? v + e * (values[h] - v) : v; }; d3.shuffle = function(array) { @@ -454,8 +451,13 @@ d3.rebind(xhr, dispatch, "on"); if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, mimeType = null; - return callback == null ? xhr : xhr.get(callback); + return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); }; + function d3_xhr_fixCallback(callback) { + return callback.length === 1 ? function(error, request) { + callback(error == null ? request : null); + } : callback; + } d3.text = function() { return d3.xhr.apply(d3, arguments).response(d3_text); }; @@ -1687,13 +1689,13 @@ return value; } function bind(group, groupData) { - var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), n1 = Math.max(n, m), updateNodes = [], enterNodes = [], exitNodes = [], node, nodeData; + var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; if (key) { - var nodeByKeyValue = new d3_Map(), keyValues = [], keyValue, j = groupData.length; + var nodeByKeyValue = new d3_Map(), dataByKeyValue = new d3_Map(), keyValues = [], keyValue; for (i = -1; ++i < n; ) { keyValue = key.call(node = group[i], node.__data__, i); if (nodeByKeyValue.has(keyValue)) { - exitNodes[j++] = node; + exitNodes[i] = node; } else { nodeByKeyValue.set(keyValue, node); } @@ -1701,14 +1703,13 @@ } for (i = -1; ++i < m; ) { keyValue = key.call(groupData, nodeData = groupData[i], i); - if (nodeByKeyValue.has(keyValue)) { - updateNodes[i] = node = nodeByKeyValue.get(keyValue); + if (node = nodeByKeyValue.get(keyValue)) { + updateNodes[i] = node; node.__data__ = nodeData; - enterNodes[i] = exitNodes[i] = null; - } else { + } else if (!dataByKeyValue.has(keyValue)) { enterNodes[i] = d3_selection_dataNode(nodeData); - updateNodes[i] = exitNodes[i] = null; } + dataByKeyValue.set(keyValue, nodeData); nodeByKeyValue.remove(keyValue); } for (i = -1; ++i < n; ) { @@ -1723,19 +1724,15 @@ if (node) { node.__data__ = nodeData; updateNodes[i] = node; - enterNodes[i] = exitNodes[i] = null; } else { enterNodes[i] = d3_selection_dataNode(nodeData); - updateNodes[i] = exitNodes[i] = null; } } for (;i < m; ++i) { enterNodes[i] = d3_selection_dataNode(groupData[i]); - updateNodes[i] = exitNodes[i] = null; } - for (;i < n1; ++i) { + for (;i < n; ++i) { exitNodes[i] = group[i]; - enterNodes[i] = updateNodes[i] = null; } } enterNodes.update = updateNodes; @@ -3769,7 +3766,7 @@ d3.behavior.zoom = function() { var translate = [ 0, 0 ], translate0, scale = 1, scale0, scaleExtent = d3_behavior_zoomInfinity, event = d3_eventDispatch(zoom, "zoom"), x0, x1, y0, y1, touchtime; function zoom() { - this.on("mousedown.zoom", mousedown).on("mousewheel.zoom", mousewheel).on("mousemove.zoom", mousemove).on("DOMMouseScroll.zoom", mousewheel).on("dblclick.zoom", dblclick).on("touchstart.zoom", touchstart).on("touchmove.zoom", touchmove).on("touchend.zoom", touchstart); + this.on("mousedown.zoom", mousedown).on("mousemove.zoom", mousemove).on(d3_behavior_zoomWheel + ".zoom", mousewheel).on("dblclick.zoom", dblclick).on("touchstart.zoom", touchstart).on("touchmove.zoom", touchmove).on("touchend.zoom", touchstart); } zoom.translate = function(x) { if (!arguments.length) return translate; @@ -3901,21 +3898,14 @@ } return d3.rebind(zoom, event, "on"); }; - var d3_behavior_zoomDiv, d3_behavior_zoomInfinity = [ 0, Infinity ]; - function d3_behavior_zoomDelta() { - if (!d3_behavior_zoomDiv) { - d3_behavior_zoomDiv = d3.select("body").append("div").style("visibility", "hidden").style("top", 0).style("height", 0).style("width", 0).style("overflow-y", "scroll").append("div").style("height", "2000px").node().parentNode; - } - var e = d3.event, delta; - try { - d3_behavior_zoomDiv.scrollTop = 1e3; - d3_behavior_zoomDiv.dispatchEvent(e); - delta = 1e3 - d3_behavior_zoomDiv.scrollTop; - } catch (error) { - delta = e.wheelDelta || -e.detail * 5; - } - return delta; - } + var d3_behavior_zoomInfinity = [ 0, Infinity ]; + var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in document ? (d3_behavior_zoomDelta = function() { + return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); + }, "wheel") : "onmousewheel" in document ? (d3_behavior_zoomDelta = function() { + return d3.event.wheelDelta; + }, "mousewheel") : (d3_behavior_zoomDelta = function() { + return -d3.event.detail; + }, "MozMousePixelScroll"); d3.layout = {}; d3.layout.bundle = function() { return function(links) { @@ -4625,10 +4615,8 @@ } d3.layout.hierarchy = function() { var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; - function recurse(data, depth, nodes) { - var childs = children.call(hierarchy, data, depth), node = d3_layout_hierarchyInline ? data : { - data: data - }; + function recurse(node, depth, nodes) { + var childs = children.call(hierarchy, node, depth); node.depth = depth; nodes.push(node); if (childs && (n = childs.length)) { @@ -4642,7 +4630,7 @@ if (sort) c.sort(sort); if (value) node.value = v; } else if (value) { - node.value = +value.call(hierarchy, data, depth) || 0; + node.value = +value.call(hierarchy, node, depth) || 0; } return node; } @@ -4652,7 +4640,7 @@ var i = -1, n, j = depth + 1; while (++i < n) v += revalue(children[i], j); } else if (value) { - v = +value.call(hierarchy, d3_layout_hierarchyInline ? node : node.data, depth) || 0; + v = +value.call(hierarchy, node, depth) || 0; } if (value) node.value = v; return v; @@ -4685,11 +4673,8 @@ }; function d3_layout_hierarchyRebind(object, hierarchy) { d3.rebind(object, hierarchy, "sort", "children", "value"); + object.nodes = object; object.links = d3_layout_hierarchyLinks; - object.nodes = function(d) { - d3_layout_hierarchyInline = true; - return (object.nodes = object)(d); - }; return object; } function d3_layout_hierarchyChildren(d) { @@ -4711,7 +4696,6 @@ }); })); } - var d3_layout_hierarchyInline = false; d3.layout.pack = function() { var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ]; function pack(d, i) { @@ -5096,7 +5080,7 @@ function squarify(node) { var children = node.children; if (children && children.length) { - var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" || mode === "slice-dice" && node.depth & 1 ? rect.dy : Math.min(rect.dx, rect.dy), n; + var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; scale(remaining, rect.dx * rect.dy / node.value); row.area = 0; while ((n = remaining.length) > 0) { @@ -5333,78 +5317,168 @@ d3.csv = d3_dsv(",", "text/csv"); d3.tsv = d3_dsv(" ", "text/tab-separated-values"); d3.geo = {}; - function d3_geo_type(types) { - for (var type in d3_geo_typeDefaults) { - if (!(type in types)) { - types[type] = d3_geo_typeDefaults[type]; + d3.geo.stream = function(object, listener) { + if (d3_geo_streamObjectType.hasOwnProperty(object.type)) { + d3_geo_streamObjectType[object.type](object, listener); + } else { + d3_geo_streamGeometry(object, listener); + } + }; + function d3_geo_streamGeometry(geometry, listener) { + if (d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { + d3_geo_streamGeometryType[geometry.type](geometry, listener); + } + } + var d3_geo_streamObjectType = { + Feature: function(feature, listener) { + d3_geo_streamGeometry(feature.geometry, listener); + }, + FeatureCollection: function(object, listener) { + var features = object.features, i = -1, n = features.length; + while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); + } + }; + var d3_geo_streamGeometryType = { + Sphere: function(object, listener) { + listener.sphere(); + }, + Point: function(object, listener) { + var coordinate = object.coordinates; + listener.point(coordinate[0], coordinate[1]); + }, + MultiPoint: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length, coordinate; + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1]); + }, + LineString: function(object, listener) { + d3_geo_streamLine(object.coordinates, listener, 0); + }, + MultiLineString: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); + }, + Polygon: function(object, listener) { + d3_geo_streamPolygon(object.coordinates, listener); + }, + MultiPolygon: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); + }, + GeometryCollection: function(object, listener) { + var geometries = object.geometries, i = -1, n = geometries.length; + while (++i < n) d3_geo_streamGeometry(geometries[i], listener); + } + }; + function d3_geo_streamLine(coordinates, listener, closed) { + var i = -1, n = coordinates.length - closed, coordinate; + listener.lineStart(); + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1]); + listener.lineEnd(); + } + function d3_geo_streamPolygon(coordinates, listener) { + var i = -1, n = coordinates.length; + listener.polygonStart(); + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); + listener.polygonEnd(); + } + function d3_geo_spherical(cartesian) { + return [ Math.atan2(cartesian[1], cartesian[0]), Math.asin(Math.max(-1, Math.min(1, cartesian[2]))) ]; + } + function d3_geo_sphericalEqual(a, b) { + return Math.abs(a[0] - b[0]) < ε && Math.abs(a[1] - b[1]) < ε; + } + function d3_geo_cartesian(spherical) { + var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); + return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; + } + function d3_geo_cartesianDot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + function d3_geo_cartesianCross(a, b) { + return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; + } + function d3_geo_cartesianAdd(a, b) { + a[0] += b[0]; + a[1] += b[1]; + a[2] += b[2]; + } + function d3_geo_cartesianScale(vector, k) { + return [ vector[0] * k, vector[1] * k, vector[2] * k ]; + } + function d3_geo_cartesianNormalize(d) { + var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + d[0] /= l; + d[1] /= l; + d[2] /= l; + } + function d3_geo_resample(project) { + var δ2 = .5, maxDepth = 16; + function resample(stream) { + var λ0, x0, y0, a0, b0, c0; + var resample = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + stream.polygonStart(); + resample.lineStart = polygonLineStart; + }, + polygonEnd: function() { + stream.polygonEnd(); + resample.lineStart = lineStart; + } + }; + function point(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + } + function lineStart() { + x0 = NaN; + resample.point = linePoint; + stream.lineStart(); + } + function linePoint(λ, φ) { + var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); + resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); + stream.point(x0, y0); + } + function lineEnd() { + resample.point = point; + stream.lineEnd(); + } + function polygonLineStart() { + var λ00, φ00, x00, y00, a00, b00, c00; + lineStart(); + resample.point = function(λ, φ) { + linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; + resample.point = linePoint; + }; + resample.lineEnd = function() { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); + resample.lineEnd = lineEnd; + lineEnd(); + }; + } + return resample; + } + function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { + var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; + if (d2 > 4 * δ2 && depth--) { + var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = Math.abs(Math.abs(c) - 1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; + if (dz * dz / d2 > δ2 || Math.abs((dx * dx2 + dy * dy2) / d2 - .5) > .3) { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); + stream.point(x2, y2); + resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); + } } } - return types; + resample.precision = function(_) { + if (!arguments.length) return Math.sqrt(δ2); + maxDepth = (δ2 = _ * _) > 0 && 16; + return resample; + }; + return resample; } - var d3_geo_typeDefaults = { - Feature: function(feature) { - this.geometry(feature.geometry); - }, - FeatureCollection: function(collection) { - var features = collection.features, i = -1, n = features.length; - while (++i < n) this.Feature(features[i]); - }, - GeometryCollection: function(collection) { - var geometries = collection.geometries, i = -1, n = geometries.length; - while (++i < n) this.geometry(geometries[i]); - }, - LineString: function(lineString) { - this.line(lineString.coordinates); - }, - MultiLineString: function(multiLineString) { - var coordinates = multiLineString.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.line(coordinates[i]); - }, - MultiPoint: function(multiPoint) { - var coordinates = multiPoint.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.point(coordinates[i]); - }, - MultiPolygon: function(multiPolygon) { - var coordinates = multiPolygon.coordinates, i = -1, n = coordinates.length; - while (++i < n) this.polygon(coordinates[i]); - }, - Point: function(point) { - this.point(point.coordinates); - }, - Polygon: function(polygon) { - this.polygon(polygon.coordinates); - }, - Sphere: d3_noop, - object: function(object) { - return d3_geo_typeObjects.hasOwnProperty(object.type) ? this[object.type](object) : this.geometry(object); - }, - geometry: function(geometry) { - return d3_geo_typeGeometries.hasOwnProperty(geometry.type) ? this[geometry.type](geometry) : null; - }, - point: d3_noop, - line: function(coordinates) { - var i = -1, n = coordinates.length; - while (++i < n) this.point(coordinates[i]); - }, - polygon: function(coordinates) { - var i = -1, n = coordinates.length; - while (++i < n) this.line(coordinates[i]); - } - }; - var d3_geo_typeGeometries = { - LineString: 1, - MultiLineString: 1, - MultiPoint: 1, - MultiPolygon: 1, - Point: 1, - Polygon: 1, - Sphere: 1 - }; - var d3_geo_typeObjects = { - Feature: 1, - FeatureCollection: 1, - GeometryCollection: 1 - }; d3.geo.albersUsa = function() { var lower48 = d3.geo.albers(); var alaska = d3.geo.albers().rotate([ 160, 0 ]).center([ 0, 60 ]).parallels([ 55, 65 ]); @@ -5417,15 +5491,6 @@ var lon = point[0], lat = point[1]; return lat > 50 ? alaska : lon < -140 ? hawaii : lat < 21 ? puertoRico : lower48; } - albersUsa.point = function(coordinates, context) { - return projection(coordinates).point(coordinates, context); - }; - albersUsa.line = function(coordinates, context) { - return projection(coordinates[0]).line(coordinates, context); - }; - albersUsa.polygon = function(coordinates, context) { - return projection(coordinates[0][0]).polygon(coordinates, context); - }; albersUsa.scale = function(x) { if (!arguments.length) return lower48.scale(); lower48.scale(x); @@ -5481,38 +5546,123 @@ return d3_geo_projection(d3_geo_azimuthalEquidistant); }).raw = d3_geo_azimuthalEquidistant; d3.geo.bounds = d3_geo_bounds(d3_identity); - function d3_geo_bounds(projection) { - var x0, y0, x1, y1, bounds = d3_geo_type({ - point: function(point) { - point = projection(point); - var x = point[0], y = point[1]; - if (x < x0) x0 = x; - if (x > x1) x1 = x; - if (y < y0) y0 = y; - if (y > y1) y1 = y; + function d3_geo_bounds(projectStream) { + var x0, y0, x1, y1; + var bound = { + point: boundPoint, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + bound.lineEnd = boundPolygonLineEnd; }, - polygon: function(coordinates) { - this.line(coordinates[0]); + polygonEnd: function() { + bound.point = boundPoint; } - }); + }; + function boundPoint(x, y) { + if (x < x0) x0 = x; + if (x > x1) x1 = x; + if (y < y0) y0 = y; + if (y > y1) y1 = y; + } + function boundPolygonLineEnd() { + bound.point = bound.lineEnd = d3_noop; + } return function(feature) { y1 = x1 = -(x0 = y0 = Infinity); - bounds.object(feature); + d3.geo.stream(feature, projectStream(bound)); return [ [ x0, y0 ], [ x1, y1 ] ]; }; } + d3.geo.centroid = function(object) { + d3_geo_centroidDimension = d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + d3.geo.stream(object, d3_geo_centroid); + var m; + if (d3_geo_centroidW && Math.abs(m = Math.sqrt(d3_geo_centroidX * d3_geo_centroidX + d3_geo_centroidY * d3_geo_centroidY + d3_geo_centroidZ * d3_geo_centroidZ)) > ε) { + return [ Math.atan2(d3_geo_centroidY, d3_geo_centroidX) * d3_degrees, Math.asin(Math.max(-1, Math.min(1, d3_geo_centroidZ / m))) * d3_degrees ]; + } + }; + var d3_geo_centroidDimension, d3_geo_centroidW, d3_geo_centroidX, d3_geo_centroidY, d3_geo_centroidZ; + var d3_geo_centroid = { + sphere: function() { + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + }, + point: d3_geo_centroidPoint, + lineStart: d3_geo_centroidLineStart, + lineEnd: d3_geo_centroidLineEnd, + polygonStart: function() { + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_centroid.lineStart = d3_geo_centroidRingStart; + }, + polygonEnd: function() { + d3_geo_centroid.lineStart = d3_geo_centroidLineStart; + } + }; + function d3_geo_centroidPoint(λ, φ) { + if (d3_geo_centroidDimension) return; + ++d3_geo_centroidW; + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + d3_geo_centroidX += (cosφ * Math.cos(λ) - d3_geo_centroidX) / d3_geo_centroidW; + d3_geo_centroidY += (cosφ * Math.sin(λ) - d3_geo_centroidY) / d3_geo_centroidW; + d3_geo_centroidZ += (Math.sin(φ) - d3_geo_centroidZ) / d3_geo_centroidW; + } + function d3_geo_centroidRingStart() { + var λ00, φ00; + d3_geo_centroidDimension = 1; + d3_geo_centroidLineStart(); + d3_geo_centroidDimension = 2; + var linePoint = d3_geo_centroid.point; + d3_geo_centroid.point = function(λ, φ) { + linePoint(λ00 = λ, φ00 = φ); + }; + d3_geo_centroid.lineEnd = function() { + d3_geo_centroid.point(λ00, φ00); + d3_geo_centroidLineEnd(); + d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; + }; + } + function d3_geo_centroidLineStart() { + var x0, y0, z0; + if (d3_geo_centroidDimension > 1) return; + if (d3_geo_centroidDimension < 1) { + d3_geo_centroidDimension = 1; + d3_geo_centroidW = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_centroid.point = function(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroid.point = nextPoint; + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); + d3_geo_centroidW += w; + d3_geo_centroidX += w * (x0 + (x0 = x)); + d3_geo_centroidY += w * (y0 + (y0 = y)); + d3_geo_centroidZ += w * (z0 + (z0 = z)); + } + } + function d3_geo_centroidLineEnd() { + d3_geo_centroid.point = d3_geo_centroidPoint; + } d3.geo.circle = function() { - var origin = [ 0, 0 ], angle, precision = 6, rotate, interpolate; + var origin = [ 0, 0 ], angle, precision = 6, interpolate; function circle() { - var o = typeof origin === "function" ? origin.apply(this, arguments) : origin; - rotate = d3_geo_rotation(-o[0] * d3_radians, -o[1] * d3_radians, 0); - var ring = []; + var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; interpolate(null, null, 1, { - lineTo: function(λ, φ) { - var point = rotate.invert(λ, φ); - point[0] *= d3_degrees; - point[1] *= d3_degrees; - ring.push(point); + point: function(x, y) { + ring.push(x = rotate(x, y)); + x[0] *= d3_degrees, x[1] *= d3_degrees; } }); return { @@ -5537,84 +5687,9 @@ }; return circle.angle(90); }; - function d3_geo_circleClip(degrees, rotate) { - var radians = degrees * d3_radians, cr = Math.cos(radians), interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians); - return { - point: function(coordinates, context) { - if (visible(coordinates = rotate(coordinates))) { - context.point(coordinates[0], coordinates[1]); - } - }, - line: function(coordinates, context) { - clipLine(coordinates, context); - }, - polygon: function(polygon, context) { - d3_geo_circleClipPolygon(polygon, context, clipLine, interpolate); - }, - sphere: function(context) { - d3_geo_projectionSphere(context, interpolate); - } - }; - function visible(point) { - return Math.cos(point[1]) * Math.cos(point[0]) > cr; - } - function clipLine(coordinates, context, ring) { - if (!(n = coordinates.length)) return [ ring && 0, false ]; - var point0 = rotate(coordinates[0]), point1, point2, v0 = visible(point0), v00 = ring && v0, v, n, clean = ring, area = 0, p, x0, x, y0, y; - if (clean) { - x0 = (p = d3_geo_stereographic(point0[0] + (v0 ? 0 : π), point0[1]))[0]; - y0 = p[1]; - } - if (v0) context.moveTo(point0[0], point0[1]); - for (var i = 1; i < n; i++) { - point1 = rotate(coordinates[i]); - v = visible(point1); - if (v !== v0) { - point2 = intersect(point0, point1); - if (d3_geo_circlePointsEqual(point0, point2) || d3_geo_circlePointsEqual(point1, point2)) { - point1[0] += ε; - point1[1] += ε; - v = visible(point1); - } - } - if (v !== v0) { - clean = false; - if (v0 = v) { - point2 = intersect(point1, point0); - context.moveTo(point2[0], point2[1]); - } else { - point2 = intersect(point0, point1); - context.lineTo(point2[0], point2[1]); - } - point0 = point2; - } - if (clean) { - p = d3_geo_stereographic(point1[0] + (v ? 0 : π), point1[1]); - x = p[0]; - y = p[1]; - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - } - if (v && !d3_geo_circlePointsEqual(point0, point1)) context.lineTo(point1[0], point1[1]); - point0 = point1; - } - return [ clean && area * .5, v00 && v ]; - } - function intersect(a, b) { - var pa = d3_geo_circleCartesian(a, [ 0, 0, 0 ]), pb = d3_geo_circleCartesian(b, [ 0, 0, 0 ]); - var n1 = [ 1, 0, 0 ], n2 = d3_geo_circleCross(pa, pb), n2n2 = d3_geo_circleDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; - if (!determinant) return a; - var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_circleCross(n1, n2), A = d3_geo_circleScale(n1, c1), B = d3_geo_circleScale(n2, c2); - d3_geo_circleAdd(A, B); - var u = n1xn2, w = d3_geo_circleDot(A, u), uu = d3_geo_circleDot(u, u), t = Math.sqrt(w * w - uu * (d3_geo_circleDot(A, A) - 1)), q = d3_geo_circleScale(u, (-w - t) / uu); - d3_geo_circleAdd(q, A); - return d3_geo_circleSpherical(q); - } - } function d3_geo_circleInterpolate(radians, precision) { var cr = Math.cos(radians), sr = Math.sin(radians); - return function(from, to, direction, context) { + return function(from, to, direction, listener) { if (from != null) { from = d3_geo_circleAngle(cr, from); to = d3_geo_circleAngle(cr, to); @@ -5623,38 +5698,110 @@ from = radians + direction * 2 * π; to = radians; } + var point; for (var step = direction * precision, t = from; direction > 0 ? t > to : t < to; t -= step) { - var c = Math.cos(t), s = Math.sin(t), point = d3_geo_circleSpherical([ cr, -sr * c, -sr * s ]); - context.lineTo(point[0], point[1]); + listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); } }; } - function d3_geo_circleClipPolygon(coordinates, context, clipLine, interpolate) { - var subject = [], clip = [], segments = [], buffer = d3_geo_circleBufferSegments(clipLine), draw = [], visibleArea = 0, invisibleArea = 0, invisible = false; - coordinates.forEach(function(ring) { - var x = buffer(ring, context), ringSegments = x[1], segment, n = ringSegments.length; - if (!n) { - invisible = true; - invisibleArea += x[0][0]; - return; + function d3_geo_circleAngle(cr, point) { + var a = d3_geo_cartesian(point); + a[0] -= cr; + d3_geo_cartesianNormalize(a); + var angle = Math.acos(Math.max(-1, Math.min(1, -a[1]))); + return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); + } + function d3_geo_clip(pointVisible, clipLine, interpolate) { + return function(listener) { + var line = clipLine(listener); + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + clip.point = pointRing; + clip.lineStart = ringStart; + clip.lineEnd = ringEnd; + invisible = false; + invisibleArea = visibleArea = 0; + segments = []; + listener.polygonStart(); + }, + polygonEnd: function() { + clip.point = point; + clip.lineStart = lineStart; + clip.lineEnd = lineEnd; + segments = d3.merge(segments); + if (segments.length) { + d3_geo_clipPolygon(segments, interpolate, listener); + } else if (visibleArea < -ε || invisible && invisibleArea < -ε) { + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + listener.polygonEnd(); + segments = null; + }, + sphere: function() { + listener.polygonStart(); + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + listener.polygonEnd(); + } + }; + function point(λ, φ) { + if (pointVisible(λ, φ)) listener.point(λ, φ); } - if (x[0][0] !== false) { - visibleArea += x[0][0]; - draw.push(segment = ringSegments[0]); - var point = segment[0], n = segment.length - 1, i = 0; - context.moveTo(point[0], point[1]); - while (++i < n) context.lineTo((point = segment[i])[0], point[1]); - context.closePath(); - return; + function pointLine(λ, φ) { + line.point(λ, φ); } - if (n > 1 && x[0][1]) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); - segments = segments.concat(ringSegments.filter(d3_geo_circleSegmentLength1)); - }); - if (!segments.length) { - if (visibleArea < 0 || invisible && invisibleArea < 0) { - d3_geo_projectionSphere(context, interpolate); + function lineStart() { + clip.point = pointLine; + line.lineStart(); } - } + function lineEnd() { + clip.point = point; + line.lineEnd(); + } + var segments, visibleArea, invisibleArea, invisible; + var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), ring; + function pointRing(λ, φ) { + ringListener.point(λ, φ); + ring.push([ λ, φ ]); + } + function ringStart() { + ringListener.lineStart(); + ring = []; + } + function ringEnd() { + pointRing(ring[0][0], ring[0][1]); + ringListener.lineEnd(); + var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; + if (!n) { + invisible = true; + invisibleArea += d3_geo_clipAreaRing(ring, -1); + ring = null; + return; + } + ring = null; + if (clean & 1) { + segment = ringSegments[0]; + visibleArea += d3_geo_clipAreaRing(segment, 1); + var n = segment.length - 1, i = -1, point; + listener.lineStart(); + while (++i < n) listener.point((point = segment[i])[0], point[1]); + listener.lineEnd(); + return; + } + if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); + segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); + } + return clip; + }; + } + function d3_geo_clipPolygon(segments, interpolate, listener) { + var subject = [], clip = []; segments.forEach(function(segment) { var n = segment.length; if (n <= 1) return; @@ -5696,112 +5843,224 @@ subject.push(a); clip.push(b); }); - clip.sort(d3_geo_circleClipSort); - d3_geo_circleLinkCircular(subject); - d3_geo_circleLinkCircular(clip); + clip.sort(d3_geo_clipSort); + d3_geo_clipLinkCircular(subject); + d3_geo_clipLinkCircular(clip); if (!subject.length) return; var start = subject[0], current, points, point; while (1) { current = start; while (current.visited) if ((current = current.next) === start) return; points = current.points; - context.moveTo((point = points.shift())[0], point[1]); + listener.lineStart(); do { current.visited = current.other.visited = true; if (current.entry) { if (current.subject) { - for (var i = 0; i < points.length; i++) context.lineTo((point = points[i])[0], point[1]); + for (var i = 0; i < points.length; i++) listener.point((point = points[i])[0], point[1]); } else { - interpolate(current.point, current.next.point, 1, context); + interpolate(current.point, current.next.point, 1, listener); } current = current.next; } else { if (current.subject) { points = current.prev.points; - for (var i = points.length; --i >= 0; ) context.lineTo((point = points[i])[0], point[1]); + for (var i = points.length; --i >= 0; ) listener.point((point = points[i])[0], point[1]); } else { - interpolate(current.point, current.prev.point, -1, context); + interpolate(current.point, current.prev.point, -1, listener); } current = current.prev; } current = current.other; points = current.points; } while (!current.visited); - context.closePath(); + listener.lineEnd(); } } - function d3_geo_circleLinkCircular(array) { - for (var i = 0, a = array[0], b, n = array.length; i < n; ) { - a.next = b = array[++i % n]; + function d3_geo_clipLinkCircular(array) { + if (!(n = array.length)) return; + var n, i = 0, a = array[0], b; + while (++i < n) { + a.next = b = array[i]; b.prev = a; a = b; } + a.next = b = array[0]; + b.prev = a; } - function d3_geo_circleClipSort(a, b) { + function d3_geo_clipSort(a, b) { return ((a = a.point)[0] < 0 ? a[1] - π / 2 - ε : π / 2 - a[1]) - ((b = b.point)[0] < 0 ? b[1] - π / 2 - ε : π / 2 - b[1]); } - function d3_geo_circleAngle(cr, point) { - var a = d3_geo_circleCartesian(point, [ cr, 0, 0 ]); - d3_geo_circleNormalize(a); - var angle = Math.acos(Math.max(-1, Math.min(1, -a[1]))); - return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); - } - function d3_geo_circleCartesian(point, origin) { - var p0 = point[0], p1 = point[1], c1 = Math.cos(p1); - return [ c1 * Math.cos(p0) - origin[0], c1 * Math.sin(p0) - origin[1], Math.sin(p1) - origin[2] ]; - } - function d3_geo_circleSpherical(point) { - return [ Math.atan2(point[1], point[0]), Math.asin(Math.max(-1, Math.min(1, point[2]))) ]; - } - function d3_geo_circleDot(a, b) { - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - } - function d3_geo_circleCross(a, b) { - return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; - } - function d3_geo_circleAdd(a, b) { - a[0] += b[0]; - a[1] += b[1]; - a[2] += b[2]; - } - function d3_geo_circleScale(vector, s) { - return [ vector[0] * s, vector[1] * s, vector[2] * s ]; - } - function d3_geo_circleNormalize(d) { - var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); - d[0] /= l; - d[1] /= l; - d[2] /= l; - } - function d3_geo_circleBufferSegments(f) { - return function(coordinates) { - var segments = [], segment; - return [ f(coordinates, { - moveTo: function(x, y) { - segments.push(segment = [ [ x, y ] ]); - }, - lineTo: function(x, y) { - segment.push([ x, y ]); - } - }, true), segments ]; - }; - } - function d3_geo_circlePointsEqual(a, b) { - return Math.abs(a[0] - b[0]) < ε && Math.abs(a[1] - b[1]) < ε; - } - function d3_geo_circleSegmentLength1(segment) { + function d3_geo_clipSegmentLength1(segment) { return segment.length > 1; } + function d3_geo_clipBufferListener() { + var lines = [], line; + return { + lineStart: function() { + lines.push(line = []); + }, + point: function(λ, φ) { + line.push([ λ, φ ]); + }, + lineEnd: d3_noop, + buffer: function() { + var buffer = lines; + lines = []; + line = null; + return buffer; + } + }; + } + function d3_geo_clipAreaRing(ring, invisible) { + if (!(n = ring.length)) return 0; + var n, i = 0, area = 0, p = ring[0], λ = p[0], φ = p[1], cosφ = Math.cos(φ), x0 = Math.atan2(invisible * Math.sin(λ) * cosφ, Math.sin(φ)), y0 = 1 - invisible * Math.cos(λ) * cosφ, x1 = x0, x, y; + while (++i < n) { + p = ring[i]; + cosφ = Math.cos(φ = p[1]); + x = Math.atan2(invisible * Math.sin(λ = p[0]) * cosφ, Math.sin(φ)); + y = 1 - invisible * Math.cos(λ) * cosφ; + if (Math.abs(y0 - 2) < ε && Math.abs(y - 2) < ε) continue; + if (Math.abs(y) < ε || Math.abs(y0) < ε) {} else if (Math.abs(Math.abs(x - x0) - π) < ε) { + if (y + y0 > 2) area += 4 * (x - x0); + } else if (Math.abs(y0 - 2) < ε) area += 4 * (x - x1); else area += ((3 * π + x - x0) % (2 * π) - π) * (y0 + y); + x1 = x0, x0 = x, y0 = y; + } + return area; + } + var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate); + function d3_geo_clipAntimeridianLine(listener) { + var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; + return { + lineStart: function() { + listener.lineStart(); + clean = 1; + }, + point: function(λ1, φ1) { + var sλ1 = λ1 > 0 ? π : -π, dλ = Math.abs(λ1 - λ0); + if (Math.abs(dλ - π) < ε) { + listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? π / 2 : -π / 2); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + listener.point(λ1, φ0); + clean = 0; + } else if (sλ0 !== sλ1 && dλ >= π) { + if (Math.abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; + if (Math.abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; + φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + clean = 0; + } + listener.point(λ0 = λ1, φ0 = φ1); + sλ0 = sλ1; + }, + lineEnd: function() { + listener.lineEnd(); + λ0 = φ0 = NaN; + }, + clean: function() { + return 2 - clean; + } + }; + } + function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { + var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); + return Math.abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; + } + function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { + var φ; + if (from == null) { + φ = direction * π / 2; + listener.point(-π, φ); + listener.point(0, φ); + listener.point(π, φ); + listener.point(π, 0); + listener.point(π, -φ); + listener.point(0, -φ); + listener.point(-π, -φ); + listener.point(-π, 0); + listener.point(-π, φ); + } else if (Math.abs(from[0] - to[0]) > ε) { + var s = (from[0] < to[0] ? 1 : -1) * π; + φ = direction * s / 2; + listener.point(-s, φ); + listener.point(0, φ); + listener.point(s, φ); + } else { + listener.point(to[0], to[1]); + } + } + function d3_geo_clipCircle(degrees) { + var radians = degrees * d3_radians, cr = Math.cos(radians), interpolate = d3_geo_circleInterpolate(radians, 6 * d3_radians); + return d3_geo_clip(visible, clipLine, interpolate); + function visible(λ, φ) { + return Math.cos(λ) * Math.cos(φ) > cr; + } + function clipLine(listener) { + var point0, v0, v00, clean; + return { + lineStart: function() { + v00 = v0 = false; + clean = 1; + }, + point: function(λ, φ) { + var point1 = [ λ, φ ], point2, v = visible(λ, φ); + if (!point0 && (v00 = v0 = v)) listener.lineStart(); + if (v !== v0) { + point2 = intersect(point0, point1); + if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { + point1[0] += ε; + point1[1] += ε; + v = visible(point1[0], point1[1]); + } + } + if (v !== v0) { + clean = 0; + if (v0 = v) { + listener.lineStart(); + point2 = intersect(point1, point0); + listener.point(point2[0], point2[1]); + } else { + point2 = intersect(point0, point1); + listener.point(point2[0], point2[1]); + listener.lineEnd(); + } + point0 = point2; + } + if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) listener.point(point1[0], point1[1]); + point0 = point1; + }, + lineEnd: function() { + if (v0) listener.lineEnd(); + point0 = null; + }, + clean: function() { + return clean | (v00 && v0) << 1; + } + }; + } + function intersect(a, b) { + var pa = d3_geo_cartesian(a, 0), pb = d3_geo_cartesian(b, 0); + var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; + if (!determinant) return a; + var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); + d3_geo_cartesianAdd(A, B); + var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t = Math.sqrt(w * w - uu * (d3_geo_cartesianDot(A, A) - 1)), q = d3_geo_cartesianScale(u, (-w - t) / uu); + d3_geo_cartesianAdd(q, A); + return d3_geo_spherical(q); + } + } function d3_geo_compose(a, b) { - if (a === d3_geo_equirectangular) return b; - if (b === d3_geo_equirectangular) return a; - function compose(λ, φ) { - var coordinates = a(λ, φ); - return b(coordinates[0], coordinates[1]); + function compose(x, y) { + return x = a(x, y), b(x[0], x[1]); } if (a.invert && b.invert) compose.invert = function(x, y) { - var coordinates = b.invert(x, y); - return a.invert(coordinates[0], coordinates[1]); + return x = b.invert(x, y), x && a.invert(x[0], x[1]); }; return compose; } @@ -5821,12 +6080,15 @@ var x1, x0, y1, y0, dx = 22.5, dy = dx, x, y, precision = 2.5; function graticule() { return { - type: "GeometryCollection", - geometries: graticule.lines() + type: "MultiLineString", + coordinates: lines() }; } + function lines() { + return d3.range(Math.ceil(x0 / dx) * dx, x1, dx).map(x).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).map(y)); + } graticule.lines = function() { - return d3.range(Math.ceil(x0 / dx) * dx, x1, dx).map(x).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).map(y)).map(function(coordinates) { + return lines().map(function(coordinates) { return { type: "LineString", coordinates: coordinates @@ -5877,11 +6139,23 @@ }); }; } + d3.geo.interpolate = function(source, target) { + return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); + }; + function d3_geo_interpolate(x0, y0, x1, y1) { + var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0)))), k = 1 / Math.sin(d); + function interpolate(t) { + var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; + return [ Math.atan2(y, x) / d3_radians, Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians ]; + } + interpolate.distance = d; + return interpolate; + } d3.geo.greatArc = function() { - var source = d3_source, p0, target = d3_target, p1, precision = 6 * d3_radians, interpolate = d3_geo_greatArcInterpolator(); + var source = d3_source, source_, target = d3_target, target_, precision = 6 * d3_radians, interpolate; function greatArc() { - var d = greatArc.distance.apply(this, arguments), t = 0, dt = precision / d, coordinates = [ p0 ]; - while ((t += dt) < 1) coordinates.push(interpolate(t)); + var p0 = source_ || source.apply(this, arguments), p1 = target_ || target.apply(this, arguments), i = interpolate || d3.geo.interpolate(p0, p1), t = 0, dt = precision / i.distance, coordinates = [ p0 ]; + while ((t += dt) < 1) coordinates.push(i(t)); coordinates.push(p1); return { type: "LineString", @@ -5889,20 +6163,18 @@ }; } greatArc.distance = function() { - if (typeof source === "function") interpolate.source(p0 = source.apply(this, arguments)); - if (typeof target === "function") interpolate.target(p1 = target.apply(this, arguments)); - return interpolate.distance(); + return (interpolate || d3.geo.interpolate(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments))).distance; }; greatArc.source = function(_) { if (!arguments.length) return source; - source = _; - if (typeof source !== "function") interpolate.source(p0 = source); + source = _, source_ = typeof _ === "function" ? null : _; + interpolate = source_ && target_ ? d3.geo.interpolate(source_, target_) : null; return greatArc; }; greatArc.target = function(_) { if (!arguments.length) return target; - target = _; - if (typeof target !== "function") interpolate.target(p1 = target); + target = _, target_ = typeof _ === "function" ? null : _; + interpolate = source_ && target_ ? d3.geo.interpolate(source_, target_) : null; return greatArc; }; greatArc.precision = function(_) { @@ -5912,36 +6184,6 @@ }; return greatArc; }; - function d3_geo_greatArcInterpolator() { - var x0, y0, cy0, sy0, kx0, ky0, x1, y1, cy1, sy1, kx1, ky1, d, k; - function interpolate(t) { - var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; - return [ Math.atan2(y, x) / d3_radians, Math.atan2(z, Math.sqrt(x * x + y * y)) / d3_radians ]; - } - interpolate.distance = function() { - if (d == null) k = 1 / Math.sin(d = Math.acos(Math.max(-1, Math.min(1, sy0 * sy1 + cy0 * cy1 * Math.cos(x1 - x0))))); - return d; - }; - interpolate.source = function(_) { - var cx0 = Math.cos(x0 = _[0] * d3_radians), sx0 = Math.sin(x0); - cy0 = Math.cos(y0 = _[1] * d3_radians); - sy0 = Math.sin(y0); - kx0 = cy0 * cx0; - ky0 = cy0 * sx0; - d = null; - return interpolate; - }; - interpolate.target = function(_) { - var cx1 = Math.cos(x1 = _[0] * d3_radians), sx1 = Math.sin(x1); - cy1 = Math.cos(y1 = _[1] * d3_radians); - sy1 = Math.sin(y1); - kx1 = cy1 * cx1; - ky1 = cy1 * sx1; - d = null; - return interpolate; - }; - return interpolate; - } function d3_geo_mercator(λ, φ) { return [ λ / (2 * π), Math.max(-.5, Math.min(+.5, Math.log(Math.tan(π / 4 + φ / 2)) / (2 * π))) ]; } @@ -5958,203 +6200,290 @@ return d3_geo_projection(d3_geo_orthographic); }).raw = d3_geo_orthographic; d3.geo.path = function() { - var pointRadius = 4.5, pointCircle = d3_geo_pathCircle(pointRadius), projection = d3.geo.albersUsa(), bounds, buffer = []; - var bufferContext = { - point: function(x, y) { - buffer.push("M", x, ",", y, pointCircle); - }, - moveTo: function(x, y) { - buffer.push("M", x, ",", y); - }, - lineTo: function(x, y) { - buffer.push("L", x, ",", y); - }, - closePath: function() { - buffer.push("Z"); - } - }; - var area, centroidWeight, x00, y00, x0, y0, cx, cy; - var areaContext = { - point: d3_noop, - moveTo: moveTo, - lineTo: function(x, y) { - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - }, - closePath: closePath - }; - var lineCentroidContext = { - point: function(x, y) { - cx += x; - cy += y; - ++centroidWeight; - }, - moveTo: moveTo, - lineTo: function(x, y) { - var dx = x - x0, dy = y - y0, δ = Math.sqrt(dx * dx + dy * dy); - centroidWeight += δ; - cx += δ * (x0 + x) / 2; - cy += δ * (y0 + y) / 2; - x0 = x; - y0 = y; - }, - closePath: closePath - }; - var polygonCentroidContext = { - point: d3_noop, - moveTo: moveTo, - lineTo: function(x, y) { - var δ = y0 * x - x0 * y; - centroidWeight += δ * 3; - cx += δ * (x0 + x); - cy += δ * (y0 + y); - x0 = x; - y0 = y; - }, - closePath: closePath - }; - function moveTo(x, y) { - x00 = x0 = x; - y00 = y0 = y; - } - function closePath() { - this.lineTo(x00, y00); - } - var context = bufferContext; + var pointRadius = 4.5, projection, context, projectStream, contextStream; function path(object) { - var result = null; - if (object != result) { - if (typeof pointRadius === "function") pointCircle = d3_geo_pathCircle(pointRadius.apply(this, arguments)); - pathType.object(object); - if (buffer.length) result = buffer.join(""), buffer = []; - } - return result; - } - var pathType = d3_geo_type({ - line: function(coordinates) { - projection.line(coordinates, context); - }, - polygon: function(coordinates) { - projection.polygon(coordinates, context); - }, - point: function(coordinates) { - projection.point(coordinates, context); - }, - Sphere: function() { - projection.sphere(context); - } - }); - var areaType = d3_geo_type({ - Feature: function(feature) { - return areaType.geometry(feature.geometry); - }, - FeatureCollection: function(collection) { - return d3.sum(collection.features, areaType.Feature); - }, - GeometryCollection: function(collection) { - return d3.sum(collection.geometries, areaType.geometry); - }, - LineString: d3_zero, - MultiLineString: d3_zero, - MultiPoint: d3_zero, - MultiPolygon: function(multiPolygon) { - return d3.sum(multiPolygon.coordinates, polygonArea); - }, - Point: d3_zero, - Polygon: function(polygon) { - return polygonArea(polygon.coordinates); - }, - Sphere: sphereArea - }); - function polygonArea(coordinates) { - area = 0; - projection.polygon(coordinates, areaContext); - return Math.abs(area) / 2; - } - function sphereArea() { - area = 0; - projection.sphere(areaContext); - return Math.abs(area) / 2; + if (object) d3.geo.stream(object, projectStream(contextStream.pointRadius(typeof pointRadius === "function" ? +pointRadius.apply(this, arguments) : pointRadius))); + return contextStream.result(); } path.area = function(object) { - return areaType.object(object); - }; - var centroidType = d3_geo_type({ - Feature: function(feature) { - return centroidType.geometry(feature.geometry); - }, - LineString: weightedCentroid(function(lineString) { - projection.line(lineString.coordinates, lineCentroidContext); - }), - MultiLineString: weightedCentroid(function(multiLineString) { - var coordinates = multiLineString.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.line(coordinates[i], lineCentroidContext); - }), - MultiPoint: weightedCentroid(function(multiPoint) { - var coordinates = multiPoint.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.point(coordinates[i], lineCentroidContext); - }), - MultiPolygon: weightedCentroid(function(multiPolygon) { - var coordinates = multiPolygon.coordinates, i = -1, n = coordinates.length; - while (++i < n) projection.polygon(coordinates[i], polygonCentroidContext); - }), - Point: weightedCentroid(function(point) { - projection.point(point.coordinates, lineCentroidContext); - }), - Polygon: weightedCentroid(function(polygon) { - projection.polygon(polygon.coordinates, polygonCentroidContext); - }), - Sphere: weightedCentroid(function() { - projection.sphere(polygonCentroidContext); - }) - }); - function weightedCentroid(f) { - return function() { - centroidWeight = cx = cy = 0; - f.apply(this, arguments); - return centroidWeight ? [ cx / centroidWeight, cy / centroidWeight ] : null; - }; - } - path.bounds = function(object) { - return (bounds || (bounds = d3_geo_bounds(projection)))(object); + d3_geo_pathAreaSum = 0; + d3.geo.stream(object, projectStream(d3_geo_pathArea)); + return d3_geo_pathAreaSum; }; path.centroid = function(object) { - return centroidType.object(object); + d3_geo_centroidDimension = d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); + return d3_geo_centroidZ ? [ d3_geo_centroidX / d3_geo_centroidZ, d3_geo_centroidY / d3_geo_centroidZ ] : undefined; + }; + path.bounds = function(object) { + return d3_geo_bounds(projectStream)(object); }; path.projection = function(_) { if (!arguments.length) return projection; - projection = _; - bounds = null; + projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; return path; }; path.context = function(_) { - if (!arguments.length) return context === bufferContext ? null : context; - context = _; - if (context == null) context = bufferContext; + if (!arguments.length) return context; + contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); return path; }; - path.pointRadius = function(x) { + path.pointRadius = function(_) { if (!arguments.length) return pointRadius; - if (typeof x === "function") pointRadius = x; else pointCircle = d3_geo_pathCircle(pointRadius = +x); + pointRadius = typeof _ === "function" ? _ : +_; return path; }; - return path; + return path.projection(d3.geo.albersUsa()).context(null); }; function d3_geo_pathCircle(radius) { return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + +2 * radius + "z"; } - var d3_geo_pathIdentity = d3.geo.path().projection({ - polygon: function(polygon, context) { - polygon.forEach(function(ring) { - var n = ring.length, i = 0, point; - context.moveTo((point = ring[0])[0], point[1]); - while (++i < n) context.lineTo((point = ring[i])[0], point[1]); - context.closePath(); - }); + function d3_geo_pathProjectStream(project) { + var resample = d3_geo_resample(function(λ, φ) { + return project([ λ * d3_degrees, φ * d3_degrees ]); + }); + return function(stream) { + stream = resample(stream); + return { + point: function(λ, φ) { + stream.point(λ * d3_radians, φ * d3_radians); + }, + sphere: function() { + stream.sphere(); + }, + lineStart: function() { + stream.lineStart(); + }, + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); + } + }; + }; + } + function d3_geo_pathBuffer() { + var pointCircle = d3_geo_pathCircle(4.5), buffer = []; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointCircle = d3_geo_pathCircle(_); + return stream; + }, + result: function() { + if (buffer.length) { + var result = buffer.join(""); + buffer = []; + return result; + } + } + }; + function point(x, y) { + buffer.push("M", x, ",", y, pointCircle); } - }); - d3.geo.centroid = d3_geo_pathIdentity.centroid; + function pointLineStart(x, y) { + buffer.push("M", x, ",", y); + stream.point = pointLine; + } + function pointLine(x, y) { + buffer.push("L", x, ",", y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + buffer.push("Z"); + } + return stream; + } + function d3_geo_pathContext(context) { + var pointRadius = 4.5; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointRadius = _; + return stream; + }, + result: d3_noop + }; + function point(x, y) { + context.moveTo(x, y); + context.arc(x, y, pointRadius, 0, 2 * π); + } + function pointLineStart(x, y) { + context.moveTo(x, y); + stream.point = pointLine; + } + function pointLine(x, y) { + context.lineTo(x, y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + context.closePath(); + } + return stream; + } + var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_pathAreaPolygon = 0; + d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; + }, + polygonEnd: function() { + d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; + d3_geo_pathAreaSum += Math.abs(d3_geo_pathAreaPolygon / 2); + } + }; + function d3_geo_pathAreaRingStart() { + var x00, y00, x0, y0; + d3_geo_pathArea.point = function(x, y) { + d3_geo_pathArea.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + d3_geo_pathAreaPolygon += y0 * x - x0 * y; + x0 = x, y0 = y; + } + d3_geo_pathArea.lineEnd = function() { + nextPoint(x00, y00); + }; + } + var d3_geo_pathCentroid = { + point: d3_geo_pathCentroidPoint, + lineStart: d3_geo_pathCentroidLineStart, + lineEnd: d3_geo_pathCentroidLineEnd, + polygonStart: function() { + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; + }, + polygonEnd: function() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; + d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; + } + }; + function d3_geo_pathCentroidPoint(x, y) { + if (d3_geo_centroidDimension) return; + d3_geo_centroidX += x; + d3_geo_centroidY += y; + ++d3_geo_centroidZ; + } + function d3_geo_pathCentroidLineStart() { + var x0, y0; + if (d3_geo_centroidDimension !== 1) { + if (d3_geo_centroidDimension < 1) { + d3_geo_centroidDimension = 1; + d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } else return; + } + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + x0 = x, y0 = y; + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX += z * (x0 + x) / 2; + d3_geo_centroidY += z * (y0 + y) / 2; + d3_geo_centroidZ += z; + x0 = x, y0 = y; + } + } + function d3_geo_pathCentroidLineEnd() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + } + function d3_geo_pathCentroidRingStart() { + var x00, y00, x0, y0; + if (d3_geo_centroidDimension < 2) { + d3_geo_centroidDimension = 2; + d3_geo_centroidX = d3_geo_centroidY = d3_geo_centroidZ = 0; + } + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + var z = y0 * x - x0 * y; + d3_geo_centroidX += z * (x0 + x); + d3_geo_centroidY += z * (y0 + y); + d3_geo_centroidZ += z * 3; + x0 = x, y0 = y; + } + d3_geo_pathCentroid.lineEnd = function() { + nextPoint(x00, y00); + }; + } + d3.geo.area = function(object) { + d3_geo_areaSum = 0; + d3.geo.stream(object, d3_geo_area); + return d3_geo_areaSum; + }; + var d3_geo_areaSum, d3_geo_areaRing; + var d3_geo_area = { + sphere: function() { + d3_geo_areaSum += 4 * π; + }, + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_areaRing = 0; + d3_geo_area.lineStart = d3_geo_areaRingStart; + }, + polygonEnd: function() { + d3_geo_areaSum += d3_geo_areaRing < 0 ? 4 * π + d3_geo_areaRing : d3_geo_areaRing; + d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; + } + }; + function d3_geo_areaRingStart() { + var λ00, φ00, λ1, λ0, φ0, cosφ0, sinφ0; + d3_geo_area.point = function(λ, φ) { + d3_geo_area.point = nextPoint; + λ1 = λ0 = (λ00 = λ) * d3_radians, φ0 = (φ00 = φ) * d3_radians, cosφ0 = Math.cos(φ0), + sinφ0 = Math.sin(φ0); + }; + function nextPoint(λ, φ) { + λ *= d3_radians, φ *= d3_radians; + if (Math.abs(Math.abs(φ0) - π / 2) < ε && Math.abs(Math.abs(φ) - π / 2) < ε) return; + var cosφ = Math.cos(φ), sinφ = Math.sin(φ); + if (Math.abs(φ0 - π / 2) < ε) d3_geo_areaRing += (λ - λ1) * 2; else { + var dλ = λ - λ0, cosdλ = Math.cos(dλ), d = Math.atan2(Math.sqrt((d = cosφ * Math.sin(dλ)) * d + (d = cosφ0 * sinφ - sinφ0 * cosφ * cosdλ) * d), sinφ0 * sinφ + cosφ0 * cosφ * cosdλ), s = (d + π + φ0 + φ) / 4; + d3_geo_areaRing += (dλ < 0 && dλ > -π || dλ > π ? -4 : 4) * Math.atan(Math.sqrt(Math.abs(Math.tan(s) * Math.tan(s - d / 2) * Math.tan(s - π / 4 - φ0 / 2) * Math.tan(s - π / 4 - φ / 2)))); + } + λ1 = λ0, λ0 = λ, φ0 = φ, cosφ0 = cosφ, sinφ0 = sinφ; + } + d3_geo_area.lineEnd = function() { + nextPoint(λ00, φ00); + }; + } d3.geo.projection = d3_geo_projection; d3.geo.projectionMutator = d3_geo_projectionMutator; function d3_geo_projection(project) { @@ -6163,86 +6492,26 @@ })(); } function d3_geo_projectionMutator(projectAt) { - var project, rotate, projectRotate, k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx = x, δy = y, δ2 = .5, clip = d3_geo_projectionCutAntemeridian(rotatePoint), clipAngle = null, context; - function projection(coordinates) { - coordinates = projectRotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); - return [ coordinates[0] * k + δx, δy - coordinates[1] * k ]; + var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { + x = project(x, y); + return [ x[0] * k + δx, δy - x[1] * k ]; + }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, clip = d3_geo_clipAntimeridian, clipAngle = null; + function projection(point) { + point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); + return [ point[0] * k + δx, δy - point[1] * k ]; } - function invert(coordinates) { - coordinates = projectRotate.invert((coordinates[0] - δx) / k, (δy - coordinates[1]) / k); - return [ coordinates[0] * d3_degrees, coordinates[1] * d3_degrees ]; + function invert(point) { + point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); + return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; } - projection.point = function(coordinates, c) { - context = c; - clip.point(coordinates, resample); - context = null; - }; - projection.line = function(coordinates, c) { - context = c; - clip.line(coordinates, resample); - context = null; - }; - projection.polygon = function(coordinates, c) { - context = c; - clip.polygon(coordinates, resample); - context = null; - }; - projection.sphere = function(c) { - context = c; - clip.sphere(resample); - context = null; + projection.stream = function(stream) { + return d3_geo_projectionRadiansRotate(rotate, clip(projectResample(stream))); }; projection.clipAngle = function(_) { if (!arguments.length) return clipAngle; - clip = _ == null ? (clipAngle = _, d3_geo_projectionCutAntemeridian(rotatePoint)) : d3_geo_circleClip(clipAngle = +_, rotatePoint); + clip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle(clipAngle = +_); return projection; }; - var λ00, φ00, λ0, sinφ0, cosφ0, x0, y0, maxDepth = 16; - function point(λ, φ) { - var p = projectPoint(λ, φ); - context.point(p[0], p[1]); - } - function moveTo(λ, φ) { - var p = projectPoint(λ00 = λ0 = λ, φ00 = φ); - sinφ0 = Math.sin(φ); - cosφ0 = Math.cos(φ); - context.moveTo(x0 = p[0], y0 = p[1]); - } - function lineTo(λ, φ) { - var p = projectPoint(λ, φ); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x0 = p[0], y0 = p[1], λ0 = λ, sinφ0 = Math.sin(φ), cosφ0 = Math.cos(φ), maxDepth); - context.lineTo(x0, y0); - } - function resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x1, y1, λ1, sinφ1, cosφ1, depth) { - var dx = x1 - x0, dy = y1 - y0, distance2 = dx * dx + dy * dy; - if (distance2 > 4 * δ2 && depth--) { - var cosΩ = sinφ0 * sinφ1 + cosφ0 * cosφ1 * Math.cos(λ1 - λ0), k = 1 / (Math.SQRT2 * Math.sqrt(1 + cosΩ)), x = k * (cosφ0 * Math.cos(λ0) + cosφ1 * Math.cos(λ1)), y = k * (cosφ0 * Math.sin(λ0) + cosφ1 * Math.sin(λ1)), z = Math.max(-1, Math.min(1, k * (sinφ0 + sinφ1))), φ2 = Math.asin(z), zε = Math.abs(Math.abs(z) - 1), λ2 = zε < ε || zε < εε && (Math.abs(cosφ0) < εε || Math.abs(cosφ1) < εε) ? (λ0 + λ1) / 2 : Math.atan2(y, x), p = projectPoint(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x0 - x2, dy2 = y0 - y2, dz = dx * dy2 - dy * dx2; - if (dz * dz / distance2 > δ2) { - var cosφ2 = Math.cos(φ2); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, x2, y2, λ2, z, cosφ2, depth); - context.lineTo(x2, y2); - resampleLineTo(x2, y2, λ2, z, cosφ2, x1, y1, λ1, sinφ1, cosφ1, depth); - } - } - } - function closePath() { - var p = projectPoint(λ00, φ00); - resampleLineTo(x0, y0, λ0, sinφ0, cosφ0, p[0], p[1], λ00, Math.sin(φ00), Math.cos(φ00), maxDepth); - context.closePath(); - } - var resample = { - point: point, - moveTo: moveTo, - lineTo: lineTo, - closePath: closePath - }; - function rotatePoint(coordinates) { - return rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); - } - function projectPoint(λ, φ) { - var point = project(λ, φ); - return [ point[0] * k + δx, δy - point[1] * k ]; - } projection.scale = function(_) { if (!arguments.length) return k; k = +_; @@ -6267,11 +6536,7 @@ δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; return reset(); }; - projection.precision = function(_) { - if (!arguments.length) return Math.sqrt(δ2); - maxDepth = (δ2 = _ * _) > 0 && 16; - return projection; - }; + d3.rebind(projection, projectResample, "precision"); function reset() { projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); var center = project(λ, φ); @@ -6285,104 +6550,35 @@ return reset(); }; } - function d3_geo_projectionIntersectAntemeridian(λ0, φ0, λ1, φ1) { - var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); - return Math.abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; - } - function d3_geo_projectionCutAntemeridian(rotatePoint) { - var clip = { - point: function(coordinates, context) { - var point = rotatePoint(coordinates); - context.point(point[0], point[1]); + function d3_geo_projectionRadiansRotate(rotate, stream) { + return { + point: function(x, y) { + y = rotate(x * d3_radians, y * d3_radians), x = y[0]; + stream.point(x > π ? x - 2 * π : x < -π ? x + 2 * π : x, y[1]); }, - line: function(coordinates, context, ring) { - if (!(n = coordinates.length)) return [ ring && 0, false ]; - var point = rotatePoint(coordinates[0]), λ0 = point[0], φ0 = point[1], λ1, φ1, sλ0 = λ0 > 0 ? π : -π, sλ1, dλ, i = 0, n, clean = ring, area = 0, x0 = (point = d3_geo_stereographic(λ0, φ0))[0], x, y0 = point[1], y; - context.moveTo(λ0, φ0); - while (++i < n) { - point = rotatePoint(coordinates[i]); - λ1 = point[0]; - φ1 = point[1]; - sλ1 = λ1 > 0 ? π : -π; - dλ = Math.abs(λ1 - λ0); - if (Math.abs(dλ - π) < ε) { - context.lineTo(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? π / 2 : -π / 2); - context.lineTo(sλ0, φ0); - context.moveTo(sλ1, φ0); - context.lineTo(λ1, φ0); - clean = false; - } else if (sλ0 !== sλ1 && dλ >= π) { - if (Math.abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; - if (Math.abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; - φ0 = d3_geo_projectionIntersectAntemeridian(λ0, φ0, λ1, φ1); - context.lineTo(sλ0, φ0); - context.moveTo(sλ1, φ0); - clean = false; - } - if (clean) { - x = (point = d3_geo_stereographic(λ1, φ1))[0]; - y = point[1]; - area += y0 * x - x0 * y; - x0 = x; - y0 = y; - } - context.lineTo(λ0 = λ1, φ0 = φ1); - sλ0 = sλ1; - } - return [ clean && area, true ]; + sphere: function() { + stream.sphere(); }, - polygon: function(polygon, context) { - d3_geo_circleClipPolygon(polygon, context, clip.line, d3_geo_antemeridianInterpolate); + lineStart: function() { + stream.lineStart(); }, - sphere: function(context) { - d3_geo_projectionSphere(context, d3_geo_antemeridianInterpolate); + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); } }; - return clip; - } - function d3_geo_antemeridianInterpolate(from, to, direction, context) { - var φ; - if (from == null) { - φ = direction * π / 2; - context.lineTo(-π, φ); - context.lineTo(0, φ); - context.lineTo(π, φ); - context.lineTo(π, 0); - context.lineTo(π, -φ); - context.lineTo(0, -φ); - context.lineTo(-π, -φ); - context.lineTo(-π, 0); - } else if (Math.abs(from[0] - to[0]) > ε) { - var s = (from[0] < to[0] ? 1 : -1) * π; - φ = direction * s / 2; - context.lineTo(-s, φ); - context.lineTo(0, φ); - context.lineTo(s, φ); - } else { - context.lineTo(to[0], to[1]); - } - } - function d3_geo_projectionSphere(context, interpolate) { - var moved = false; - interpolate(null, null, 1, { - lineTo: function(x, y) { - (moved ? context.lineTo : (moved = true, context.moveTo))(x, y); - } - }); - context.closePath(); } function d3_geo_rotation(δλ, δφ, δγ) { - return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; + return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_equirectangular; } - function d3_geo_identityRotation(λ, φ) { - return [ λ > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; - } - d3_geo_identityRotation.invert = function(x, y) { - return [ x, y ]; - }; function d3_geo_forwardRotationλ(δλ) { return function(λ, φ) { - return [ (λ += δλ) > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; + return λ += δλ, [ λ > π ? λ - 2 * π : λ < -π ? λ + 2 * π : λ, φ ]; }; } function d3_geo_rotationλ(δλ) { diff --git a/js/lib/locale.js b/js/lib/locale.js new file mode 100644 index 000000000..8582df03e --- /dev/null +++ b/js/lib/locale.js @@ -0,0 +1,26 @@ +var locale = { _current: 'en' }; + +locale.current = function(_) { + if (!arguments.length) return locale._current; + if (locale[_] !== undefined) locale._current = _; + return locale; +}; + +function t(s, o, loc) { + loc = loc || locale._current; + + var path = s.split(".").reverse(), + rep = locale[loc]; + + while (rep !== undefined && path.length) rep = rep[path.pop()]; + + if (rep !== undefined) { + if (o) for (var k in o) rep = rep.replace('{' + k + '}', o[k]); + return rep; + } else { + var missing = 'Missing translation: ' + s; + if (console) console.error(missing); + if (loc !== 'en') return t(s, o, 'en'); + return missing; + } +} diff --git a/js/lib/queue.js b/js/lib/queue.js deleted file mode 100644 index 9a3b9da47..000000000 --- a/js/lib/queue.js +++ /dev/null @@ -1,84 +0,0 @@ -(function() { - if (typeof module === "undefined") self.queue = queue; - else module.exports = queue; - - queue.version = "1.0.0"; - - function queue(parallelism) { - var queue = {}, - active = 0, // number of in-flight deferrals - remaining = 0, // number of deferrals remaining - head, tail, // singly-linked list of deferrals - error = null, - results = [], - await = noop, - awaitAll; - - if (arguments.length < 1) parallelism = Infinity; - - queue.defer = function() { - if (!error) { - var node = arguments; - node.index = results.push(undefined) - 1; - if (tail) tail.next = node, tail = tail.next; - else head = tail = node; - ++remaining; - pop(); - } - return queue; - }; - - queue.await = function(f) { - await = f; - awaitAll = false; - if (!remaining) notify(); - return queue; - }; - - queue.awaitAll = function(f) { - await = f; - awaitAll = true; - if (!remaining) notify(); - return queue; - }; - - function pop() { - if (head && active < parallelism) { - var node = head, - f = node[0], - a = Array.prototype.slice.call(node, 1), - i = node.index; - if (head === tail) head = tail = null; - else head = head.next; - ++active; - a.push(function(e, r) { - --active; - if (error != null) return; - if (e != null) { - // clearing remaining cancels subsequent callbacks - // clearing head stops queued tasks from being executed - // setting error ignores subsequent calls to defer - error = e; - remaining = results = head = tail = null; - notify(); - } else { - results[i] = r; - if (--remaining) pop(); - else notify(); - } - }); - f.apply(null, a); - } - } - - function notify() { - if (error != null) await(error); - else if (awaitAll) await(null, results); - else await.apply(null, [null].concat(results)); - } - - return queue; - } - - function noop() {} -})(); diff --git a/locale/README.md b/locale/README.md new file mode 100644 index 000000000..0feb1f631 --- /dev/null +++ b/locale/README.md @@ -0,0 +1,49 @@ +# Translations + +At this stage in its development, iD is using an extremely minimal, simple +system for translations. This directory contains languages according to +code (de: German, fr: French, etc). + +To contribute: + +If you're technically-minded, clone this repository and edit the necessary +file, and you can preview your changes in-place if your system language is +set. Check out [the contributing guide for submitting changes](https://github.com/systemed/iD/blob/master/CONTRIBUTING.md). + +If you aren't, you can still contribute! You'll still need a GitHub account, but +you can just browse to your language's file here, +click 'Edit', and edit each translated string. + +## Translating Strings + +Let's look at an example line from `en.js`: + +```javascript +no_results: "Couldn't locate a place named '{name}'" +``` + +`no_results` is the translation _key_, and should not be translated. +The text to the right of the colon, `"Couldn't locate a place named '{name}'"`, +is the string to be translated. + +The word in brackets, `{name}`, should **not** be translated into a new +language: it's replaced with a place name when iD presents the text. So +a French translation would look like + +```javascript +no_results: "Impossible de localiser l'endroit nommé '{name}'" +``` + +For technical reasons, a few translation keys are quoted. For example: + +``` +'delete': "Delete" +``` + +Only translate the value to the right of the colon, not the quoted key on +the left. + +## License + +Contributions to translations are under the same liberal +license as iD itself, [wtfpl](http://www.wtfpl.net/). diff --git a/locale/da.js b/locale/da.js new file mode 100644 index 000000000..d42f95c61 --- /dev/null +++ b/locale/da.js @@ -0,0 +1,227 @@ +locale.da = { + modes: { + add_area: { + title: "Område", + description: "Tilføj parker, bygninger, søer, eller andre områder til kortet.", + tail: "Klik på kortet for at indtegne et område fx en park, sø eller bygning." + }, + add_line: { + title: "Linje", + description: "Linjer kan være veje, gader eller stier selv kanaler kan være linjer.", + tail: "Klik på koret for at indtegne en vej, sti eller rute." + }, + add_point: { + title: "Punkt", + description: "Restauranter, mindesmærker og postkasser er punkter.", + tail: "Klik på kortet for at tilføje et punkt." + }, + browse: { + title: "Browse", + description: "Træk rundt og zoom på kortet." + }, + draw_area: { + tail: "Klik her for at tilføje punkter til dit område. Click the first point to finish the area." + }, + draw_line: { + tail: "Click to add more points to the line. Click on other lines to connect to them, and double-click to end the line." + } + }, + + operations: { + add: { + annotation: { + point: "Tilføjede et punkt.", + vertex: "Tilføjede en node til en vej." + } + }, + start: { + annotation: { + line: "Startede en linje.", + area: "Startede et område." + } + }, + 'continue': { + annotation: { + line: "Forsatte en linje.", + area: "Forsatte et område." + } + }, + cancel_draw: { + annotation: "Annulleret indtegning." + }, + change_tags: { + annotation: "Ændret tags." + }, + circularize: { + title: "Circularize", + description: "Make this round.", + key: "O", + annotation: { + line: "Made a line circular.", + area: "Made an area circular." + } + }, + orthogonalize: { + title: "Orthogonalize", + description: "Square these corners.", + key: "Q", + annotation: { + line: "Squared the corners of a line.", + area: "Squared the corners of an area." + } + }, + 'delete': { + title: "Slet", + description: "Fjern dette fra kortet.", + key: "⌫", + annotation: { + point: "Slettede et punkt.", + vertex: "Slettede en node fra en vej.", + line: "Slettede en linje.", + area: "Slettede et område.", + relation: "Sletede en relation.", + multiple: "Slettede {n} objekter." + } + }, + connect: { + annotation: { + point: "Connected a way to a point.", + vertex: "Connected a way to another.", + line: "Connected a way to a line.", + area: "Connected a way to an area." + } + }, + disconnect: { + title: "Disconnect", + description: "Disconnect these ways from each other.", + key: "D", + annotation: "Disconnected ways." + }, + merge: { + title: "Merge", + description: "Merge these lines.", + key: "C", + annotation: "Merged {n} lines." + }, + move: { + title: "Flyt", + description: "Flyt dette til anden lokation.", + key: "M", + annotation: { + point: "Flyttede et punktMoved.", + vertex: "Flyttede en node i en vej.", + line: "Flyttede en linje.", + area: "Flyttede et område." + } + }, + reverse: { + title: "Reverse", + description: "Make this line go in the opposite direction.", + key: "V", + annotation: "Reversed a line." + }, + split: { + title: "Del op", + description: "Del op i to vej ved dette punkt.", + key: "X", + annotation: "Del op en vej." + } + }, + + nothing_to_undo: "Nothing to undo.", + nothing_to_redo: "Nothing to redo.", + + just_edited: "Du har lige rettede i OpenStreetMap!", + browser_notice: "This editor is supported in Firefox, Chrome, Safari, Opera, and Internet Explorer 9 and above. Please upgrade your browser or use Potlatch 2 to edit the map.", + view_on_osm: "Vis på OSM", + zoom_in_edit: "zoom ind for at rette kortet", + logout: "log ud", + report_a_bug: "report a bug", + + layerswitcher: { + title: "Background", + description: "Background Settings", + percent_brightness: "{opacity}% brightness", + fix_misalignment: "Fix misalignment", + reset: "nulstill" + }, + + commit: { + title: "Save Changes", + description_placeholder: "Brief description of your contributions", + upload_explanation: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + save: "Save", + cancel: "Cancel", + warnings: "Warnings", + modified: "Modified", + deleted: "Deleted", + created: "Created" + }, + + contributors: { + list: "Vis bidrag fra {users}", + truncated_list: "Vis bidrag fra {users} og {count} andre" + }, + + geocoder: { + title: "Find et sted", + placeholder: "find et sted", + no_results: "Kunne ikke finde '{name}'" + }, + + geolocate: { + title: "Show My Location" + }, + + inspector: { + no_documentation_combination: "Der er ingen dokumentation for denne tag kombination", + no_documentation_key: "Der er ingen dokumenation tilgængelig for denne nøgle", + new_tag: "Nyt Tag", + edit_tags: "Ret tags", + okay: "Ok", + view_on_osm: "View on OSM" + }, + + restore: { + description: "You have unsaved changes from a previous editing session. Do you wish to restore these changes?", + restore: "Restore", + reset: "Reset" + }, + + save: { + title: "Gem", + help: "Gem ændringer til OpenStreetMap gør dem synlige for andre brugere", + error: "Der skete en fejl da du prøvede at gemme", + uploading: "Gemmer nu ændringer til OpenStreetMap.", + unsaved_changes: "Du har ændringer der ikke er gemt endnu", + }, + + splash: { + welcome: "Welcome to the iD OpenStreetMap editor", + text: "This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "live", + dev: "dev" + }, + + tag_reference: { + description: "Description", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + zoom: { + in: "Zoom ind", + out: "Zoom ud" + }, + + validations: { + untagged_point: "Untagged point which is not part of a line or area", + untagged_line: "Untagged line", + untagged_area: "Untagged area", + tag_suggests_area: "The tag {tag} suggests line should be area, but it is not an area", + deprecated_tags: "Deprecated tags: {tags}" + } +}; diff --git a/locale/de.js b/locale/de.js new file mode 100644 index 000000000..7145dfe2b --- /dev/null +++ b/locale/de.js @@ -0,0 +1,227 @@ +locale.de = { + modes: { + add_area: { + title: "Fläche", + description: "Füge Parks, Gebäude, Seen oder andere Flächen zur Karte hinzu.", + tail: "Klicke in die Karte, um das Zeichnen einer Fläche wie einen Park, einen See oder Gebäude zu starten." + }, + add_line: { + title: "Linie", + description: "Linien können Autobahnen, Straßen, Fußwege oder sogar Kanäle sein.", + tail: "Klicke in die Karte, um das Zeichnen einer Straße eines Pfades oder einer Route zu starten." + }, + add_point: { + title: "Punkt", + description: "Restaurants, Denkmäler und Briefkästen sind Punkte", + tail: "Klicke in die Karte, um einen Punkt hinzuzufügen." + }, + browse: { + title: "Navigation", + description: "Verschieben und Vergrößern/Verkleinern des Kartenausschnitts." + }, + draw_area: { + tail: "Klicke, um Punkte zur Fläche hinzuzufügen. Klicke auf den ersten Punkt, um die Fläche abzuschließen." + }, + draw_line: { + tail: "Klicke, um mehr Punkte zur Linie hizuzufügen. Klicke auf eine andere Linie um die Linien zu verbinden und klicke doppelt, um die Linie zu beenden." + } + }, + + operations: { + add: { + annotation: { + point: "Punkt hinzugefügt.", + vertex: "Stützpunkt einem Weg hinzugefügt." + } + }, + start: { + annotation: { + line: "Linie begonnen.", + area: "Fläche begonnen." + } + }, + 'continue': { + annotation: { + line: "Linie fortgesetzt.", + area: "Fläche fortgesetzt." + } + }, + cancel_draw: { + annotation: "Zeichnen abgebrochen." + }, + change_tags: { + annotation: "Tags verändert." + }, + circularize: { + title: "Abrunden", + description: "Runde dies ab.", + key: "O", + annotation: { + line: "Runde eine Linie ab.", + area: "Runde eine Fläche ab." + } + }, + orthogonalize: { + title: "Rechtwinkligkeit herstellen", + description: "Diese Ecken rechtwinklig ausrichten.", + key: "Q", + annotation: { + line: "Die Ecken einer Linie rechtwinklig ausgerichtet.", + area: "Die Ecken einer Fläche rechtwinklig ausgerichtet." + } + }, + 'delete': { + title: "Löschen", + description: "Lösche dies aus der Karte.", + key: "⌫", + annotation: { + point: "Punkt gelöscht.", + vertex: "Stützpunkt aus einem Weg gelöscht.", + line: "Linie gelöscht.", + area: "Fläche gelöscht.", + relation: "Verbindung gelöscht.", + multiple: "{n} Objekte gelöscht." + } + }, + connect: { + annotation: { + point: "Weg mit einem Punkt verbunden.", + vertex: "Weg mit einem anderem Weg verbunden.", + line: "Weg mit einer Linie verbunden.", + area: "Weg mit einer Fläche verbunden." + } + }, + disconnect: { + title: "Trennen", + description: "Trenne diese Wege voneinander.", + key: "D", + annotation: "Wege getrennt." + }, + merge: { + title: "Vereinigen", + description: "Vereinige diese Linien.", + key: "C", + annotation: "{n} Linien vereinigt." + }, + move: { + title: "Verschieben", + description: "Verschiebe dieses Objekt an einen anderen Ort.", + key: "M", + annotation: { + point: "Punkt verschoben.", + vertex: "Stützpunkt in einen Weg veschoben.", + line: "Linie verschoben.", + area: "Fläche verschoben." + } + }, + reverse: { + title: "Umkehren", + description: "Ändere die Richtung dieser Linie.", + key: "V", + annotation: "Linienrichtung umgekehrt." + }, + split: { + title: "Teilen", + description: "Teile dies in zwei Wege an diesem Punkt.", + key: "X", + annotation: "Weg geteilt." + } + }, + + nothing_to_undo: "Nichts zum Rückgängigmachen.", + nothing_to_redo: "Nichts zum Wiederherstellen.", + + just_edited: "Sie haben gerade OpenStreetMap editiert!", + browser_notice: "Dieser Editor wird von Firefox, Chrome, Safari, Opera, und Internet Explorer (Version 9 und höher) unterstzützt. Bitte aktualisieren Sie Ihren Browser oder nutzen Sie Potlatch 2, um die Karte zu modifizieren.", + view_on_osm: "Auf OSM anschauen", + zoom_in_edit: "Hineinzoomen, um die Karte zu bearbeiten", + logout: "Abmelden", + report_a_bug: "Programmfehler melden", + + commit: { + title: "Änderungen speichern", + description_placeholder: "Eine kurze Beschreibung deiner Beiträge", + upload_explanation: "Änderungen, die du als {user} hochlädst werden sichtbar auf allen Karte, die OpenStreetMap nutzen.", + save: "Speichern", + cancel: "Abbrechen", + warnings: "Warnungen", + modified: "Verändert", + deleted: "Gelöscht", + created: "Erstellt" + }, + + contributors: { + list: "Diese Kartenansicht enthält Beiträge von:", + truncated_list: "Diese Kartenansicht enthält Beiträge von: {users} und {count} Anderen" + }, + + geocoder: { + title: "Suche einen Ort", + placeholder: "suche einen Ort", + no_results: "Der Ort '{name}' konnte nicht gefunden werden" + }, + + geolocate: { + title: "Zeige meine Position" + }, + + inspector: { + no_documentation_combination: "Für dieses Attribut ist keine Dokumentation verfügbar.", + no_documentation_key: "Für dises Schlüsselwort ist keine Dokumentation verfügbar", + new_tag: "Neues Attribut", + edit_tags: "Attribute bearbeiten", + okay: "OK", + view_on_osm: "auf OSM ansehen" + }, + + layerswitcher: { + title: "Hintergrund", + description: "Hintergrundeinstellungen", + percent_brightness: "{opacity}% Helligkeit", + fix_misalignment: "Fehlerhafte Ausrichtung reparieren", + reset: "Zurücksetzen" + }, + + restore: { + description: "Es gibt ungespeicherte Änderungen aus einer vorherigen Sitzung. Möchtest du diese Änderungen wiederherstellen?", + restore: "Wiederherstellen", + reset: "Zurücksetzen" + }, + + save: { + title: "Speichern", + help: "Speichere Änderungen auf OpenStreetMap, um diese für andere Nutzer sichtbar zu machen", + error: "Beim Speichern ist ein Fehler aufgetreten", + uploading: "Änderungen werden zu OpenStreetMap hochgeladen.", + unsaved_changes: "Ungespeicherte Änderugen vorhanden", + }, + + splash: { + welcome: "Willkommen beim iD OpenStreetMap editor", + text: "Dies ist eine Entwicklungsversion {version}. Für weitere Informationen besuche {website} und melde Fehler unter {github}." + }, + + source_switch: { + live: "live", + dev: "dev" + }, + + tag_reference: { + description: "Beschreibung", + on_wiki: "{tag} auf wiki.osm.org", + used_with: "benutzt mit {type}" + }, + + validations: { + untagged_point: "Punkt ohne Attribute, der kein Teil einer Linie oder Fläche ist", + untagged_line: "Linie ohne Attribute", + untagged_area: "Fläche ohne Attribute", + tag_suggests_area: "Das Attribut {tag} suggeriert eine Fläche, ist aber keine Fläche", + deprecated_tags: "Veralterte Attribute: {tags}" + }, + + zoom: { + in: "Hineinzoomen", + out: "Herauszoomen" + } +}; diff --git a/locale/en.js b/locale/en.js new file mode 100644 index 000000000..bd26f932b --- /dev/null +++ b/locale/en.js @@ -0,0 +1,227 @@ +locale.en = { + modes: { + add_area: { + title: "Area", + description: "Add parks, buildings, lakes, or other areas to the map.", + tail: "Click on the map to start drawing an area, like a park, lake, or building." + }, + add_line: { + title: "Line", + description: "Lines can be highways, streets, pedestrian paths, or even canals.", + tail: "Click on the map to start drawing an road, path, or route." + }, + add_point: { + title: "Point", + description: "Restaurants, monuments, and postal boxes are points.", + tail: "Click on the map to add a point." + }, + browse: { + title: "Browse", + description: "Pan and zoom the map." + }, + draw_area: { + tail: "Click to add points to your area. Click the first point to finish the area." + }, + draw_line: { + tail: "Click to add more points to the line. Click on other lines to connect to them, and double-click to end the line." + } + }, + + operations: { + add: { + annotation: { + point: "Added a point.", + vertex: "Added a node to a way." + } + }, + start: { + annotation: { + line: "Started a line.", + area: "Started an area." + } + }, + 'continue': { + annotation: { + line: "Continued a line.", + area: "Continued an area." + } + }, + cancel_draw: { + annotation: "Canceled drawing." + }, + change_tags: { + annotation: "Changed tags." + }, + circularize: { + title: "Circularize", + description: "Make this round.", + key: "O", + annotation: { + line: "Made a line circular.", + area: "Made an area circular." + } + }, + orthogonalize: { + title: "Orthogonalize", + description: "Square these corners.", + key: "Q", + annotation: { + line: "Squared the corners of a line.", + area: "Squared the corners of an area." + } + }, + 'delete': { + title: "Delete", + description: "Remove this from the map.", + key: "⌫", + annotation: { + point: "Deleted a point.", + vertex: "Deleted a node from a way.", + line: "Deleted a line.", + area: "Deleted an area.", + relation: "Deleted a relation.", + multiple: "Deleted {n} objects." + } + }, + connect: { + annotation: { + point: "Connected a way to a point.", + vertex: "Connected a way to another.", + line: "Connected a way to a line.", + area: "Connected a way to an area." + } + }, + disconnect: { + title: "Disconnect", + description: "Disconnect these ways from each other.", + key: "D", + annotation: "Disconnected ways." + }, + merge: { + title: "Merge", + description: "Merge these lines.", + key: "C", + annotation: "Merged {n} lines." + }, + move: { + title: "Move", + description: "Move this to a different location.", + key: "M", + annotation: { + point: "Moved a point.", + vertex: "Moved a node in a way.", + line: "Moved a line.", + area: "Moved an area." + } + }, + reverse: { + title: "Reverse", + description: "Make this line go in the opposite direction.", + key: "V", + annotation: "Reversed a line." + }, + split: { + title: "Split", + description: "Split this into two ways at this point.", + key: "X", + annotation: "Split a way." + } + }, + + nothing_to_undo: "Nothing to undo.", + nothing_to_redo: "Nothing to redo.", + + just_edited: "You Just Edited OpenStreetMap!", + browser_notice: "This editor is supported in Firefox, Chrome, Safari, Opera, and Internet Explorer 9 and above. Please upgrade your browser or use Potlatch 2 to edit the map.", + view_on_osm: "View on OSM", + zoom_in_edit: "zoom in to edit the map", + logout: "logout", + report_a_bug: "report a bug", + + commit: { + title: "Save Changes", + description_placeholder: "Brief description of your contributions", + upload_explanation: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + save: "Save", + cancel: "Cancel", + warnings: "Warnings", + modified: "Modified", + deleted: "Deleted", + created: "Created" + }, + + contributors: { + list: "Viewing contributions by {users}", + truncated_list: "Viewing contributions by {users} and {count} others" + }, + + geocoder: { + title: "Find A Place", + placeholder: "find a place", + no_results: "Couldn't locate a place named '{name}'" + }, + + geolocate: { + title: "Show My Location" + }, + + inspector: { + no_documentation_combination: "There is no documentation available for this tag combination", + no_documentation_key: "There is no documentation available for this key", + new_tag: "New Tag", + edit_tags: "Edit tags", + okay: "Okay", + view_on_osm: "View on OSM" + }, + + layerswitcher: { + title: "Background", + description: "Background Settings", + percent_brightness: "{opacity}% brightness", + fix_misalignment: "Fix misalignment", + reset: "reset" + }, + + restore: { + description: "You have unsaved changes from a previous editing session. Do you wish to restore these changes?", + restore: "Restore", + reset: "Reset" + }, + + save: { + title: "Save", + help: "Save changes to OpenStreetMap, making them visible to other users", + error: "An error occurred while trying to save", + uploading: "Uploading changes to OpenStreetMap.", + unsaved_changes: "You have unsaved changes" + }, + + splash: { + welcome: "Welcome to the iD OpenStreetMap editor", + text: "This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "live", + dev: "dev" + }, + + tag_reference: { + description: "Description", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + validations: { + untagged_point: "Untagged point which is not part of a line or area", + untagged_line: "Untagged line", + untagged_area: "Untagged area", + tag_suggests_area: "The tag {tag} suggests line should be area, but it is not an area", + deprecated_tags: "Deprecated tags: {tags}" + }, + + zoom: { + in: "Zoom In", + out: "Zoom Out" + } +}; diff --git a/locale/es.js b/locale/es.js new file mode 100644 index 000000000..739e3de22 --- /dev/null +++ b/locale/es.js @@ -0,0 +1,227 @@ +locale.es = { + modes: { + add_area: { + title: "Zona", //"Area", + description: "Agregar parques, edificios, lagos u otras zonas en el mapa", //"Add parks, buildings, lakes, or other areas to the map.", + tail: "Hacer click en el mapa para empezar a dibujar una zona como un parque, lago o edificio" //"Click on the map to start drawing an area, like a park, lake, or building." + }, + add_line: { + title: "Línea", //"Line", + description: "Las líneas pueden ser autopistas, calles, pasos peatonales o canales.", //"Lines can be highways, streets, pedestrian paths, or even canals.", + tail: "Hace clic para dibujar en el mapa, una calle, camino o ruta." //"Click on the map to start drawing an road, path, or route.", + }, + add_point: { + title: "Punto", //"Point", + description: "Son puntos los restaurantes, monumentos y buzones", //"Restaurants, monuments, and postal boxes are points.", + tail: "Hacer clic para agregar un punto en el mapa" //"Click on the map to add a point.", + }, + browse: { + title: "Navegar", //"Browse", + description: "Aumentar y navegar el mapa" //"Pan and zoom the map.", + }, + draw_area: { + tail: "Hacer clic para agregar puntos en tu zona. Hacer hacer click en el primer punto para finalizar la zona." //"Click to add points to your area. Click the first point to finish the area." + }, + draw_line: { + tail: "Hacer clic para agregar más puntos a la línea. Hacer clic en otras líneas para conectarlas, y doble clic para finalizar." //"Click to add more points to the line. Click on other lines to connect to them, and double-click to end the line." + } + }, + + operations: { + add: { + annotation: { + point: "Punto agregado", //"Added a point.", + vertex: "Nodo agregado a una ruta" //"Added a node to a way." + } + }, + start: { + annotation: { + line: "Línea iniciada", //"Started a line.", + area: "Zona iniciada" //"Started an area." + } + }, + 'continue': { + annotation: { + line: "Línea continuada.", //"Continued a line.", + area: "Zona continuada." //"Continued an area." + } + }, + cancel_draw: { + annotation: "Dibujo cancelado." //"Cancelled drawing." + }, + change_tags: { + annotation: "Etiquetas cambiadas." //"Changed tags." + }, + circularize: { + title: "Redondear", //"Circularize", + description: "Redondear esto.", //"Make this round.", + key: "O", + annotation: { + line: "Redondear una línea.", //"Made a line circular.", + area: "Redondear una zona." //"Made an area circular." + } + }, + orthogonalize: { + title: "Escuadrar", //"Orthogonalize", + description: "Escuadrar estas esquinas.", //"Square these corners.", + key: "E", //"Q", + annotation: { + line: "Esquinas de la línea escuadrados.", //"Squared the corners of a line.", + area: "Esquinas de la zona escuadrados." //"Squared the corners of an area." + } + }, + 'delete': { + title: "Eliminar", //"Delete", + description: "Eliminar esto del mapa.", //"Remove this from the map.", + key: "⌫", + annotation: { + point: "Punto eliminado.", //"Deleted a point.", + vertex: "Nodo elimnado de una ruta.", //"Deleted a node from a way.", + line: "Línea eliminada.", //"Deleted a line.", + area: "Zona eliminada.", //"Deleted an area.", + relation: "Relación eliminada.", //"Deleted a relation.", + multiple: "{n} objetos eliminados." //"Deleted {n} objects." + } + }, + connect: { + annotation: { + point: "Punto conectado a una ruta.", //"Connected a way to a point.", + vertex: "Ruta conectada a otra.", //"Connected a way to another.", + line: "Ruta conectada a una línea.", //"Connected a way to a line.", + area: "Ruta conectada a una zona." //"Connected a way to an area." + } + }, + disconnect: { + title: "Desconectar", //"Disconnect", + description: "Desconectar estas rutas.", //"Disconnect these ways from each other.", + key: "D", + annotation: "Rutas desconectadas." //"Disconnected ways." + }, + merge: { + title: "Combinar", //"Merge", + description: "Combinar estas líneas.", //"Merge these lines.", + key: "C", + annotation: "{n} líneas combinadas" //"Merged {n} lines." + }, + move: { + title: "Mover", //"Move", + description: "Mover esto a una ubicación diferente.", //"Move this to a different location.", + key: "M", + annotation: { + point: "Punto movido", //"Moved a point.", + vertex: "Nodo movido a una ruta", //"Moved a node in a way.", + line: "Línea movida", //"Moved a line.", + area: "Zona movida" //"Moved an area." + } + }, + reverse: { + title: "Invertir", //"Reverse", + description: "Hacer que esta línea vaya en sentido inverso.", //"Make this line go in the opposite direction.", + key: "I", //"V", + annotation: "Línea invertida" //"Reversed a line." + }, + split: { + title: "Dividir", //"Split", + description: "Dividir en dos rutas en éste punto.", //"Split this into two ways at this point.", + key: "D", //"X", + annotation: "Dividir una ruta." //"Split a way." + } + }, + + nothing_to_undo: "Nada para deshacer", //"Nothing to undo.", + nothing_to_redo: "Nada para rehacer", //"Nothing to redo.", + + just_edited: "Acabas de editar OpenStreetMap!", //"You Just Edited OpenStreetMap!", + browser_notice: "Este editor soporta Firefox, Chrome, Safari, Opera e Internet Explorer 9 o superior. Por favor actualiza tu navegador o utiliza Potlatch 2 para editar el mapa.", //"This editor is supported in Firefox, Chrome, Safari, Opera, and Internet Explorer 9 and above. Please upgrade your browser or use Potlatch 2 to edit the map.", + view_on_osm: "Ver en OSM", //"View on OSM", + zoom_in_edit: "acercar para editar el mapa", //"zoom in to edit the map", + logout: "cerrar sesión", //"logout", + report_a_bug: "reportar un error", //"report a bug", + + commit: { + title: "Guardar Cambios", // "Save Changes" + description_placeholder: "Breve descripción de tus contribuciones", //"Brief description of your contributions" + upload_explanation: "Los cambios que subes como {user} serán visibles en todos los mapas que usen datos de OpenStreetMap.", //"The changes you upload as {user} will be visible on all maps that use OpenStreetMap data." + save: "Guardar", //"Save" + cancel: "Cancelar", //"Cancel" + warnings: "Avisos", //"Warnings" + modified: "Modificado", //"Modified" + deleted: "Borrado", //"Deleted" + created: "Creado" //"Created" + }, + + contributors: { + list: "Viendo las contribuciones de usuarios {users}", //"Viewing contributions by {users}", + truncated_list: "Viendo las contribuciones de {users} y {count} más" //"Viewing contributions by {users} and {count} others" + }, + + geocoder: { + title: "Encontrar un lugar", //"Find A Place", + placeholder: "encontrar un lugar", //"find a place", + no_results: "No se pudo encontrar el lugar llamado '{name}'" //"Couldn't locate a place named '{name}'" + }, + + geolocate: { + title: "Mostrar Mi Localización" //"Show My Location" + }, + + inspector: { + no_documentation_combination: "No hay documentación disponible para esta combinación de etiquetas", //"This is no documentation available for this tag combination", + no_documentation_key: "No hay documentación disponible para esta tecla", //"This is no documentation available for this key", + new_tag: "Nueve etiqueta", //"New Tag" + edit_tags: "Editar etiquetas", //"Edit tags", + okay: "OK", + view_on_osm: "Ver en OSM" //"View on OSM" + }, + + layerswitcher: { + title: "Fondo", //"Background", + description: "Configuración de fondo", //"Background Settings", + percent_brightness: "{opacity}% brillo", //"{opacity}% brightness", + fix_misalignment: "Arreglar alineamiento", //"Fix misalignment", + reset: "reiniciar" //"reset" + }, + + restore: { + description: "Tienes cambios no guardados de una sesión de edición previa. ¿Quieres recuperar esos cambios?", //"You have unsaved changes from a previous editing session. Do you wish to restore these changes?" + restore: "Restaurar", //"Restore" + reset: "Restablecer" //"Reset" + }, + + save: { + title: "Guardar", //"Save", + help: "Guardar los cambios en OpenStreetMap haciéndolos visibles a otros usuarios", //"Save changes to OpenStreetMap, making them visible to other users", + error: "Ha ocurrido un error tratando de guardar", //"An error occurred while trying to save", + uploading: "Subiendo cambios a OpenStreetMap", //"Uploading changes to OpenStreetMap.", + unsaved_changes: "Tienes cambios sin guardar" //"You have unsaved changes", + }, + + splash: { + welcome: "Bienvenido al editor de OpenStreetMap iD", //"Welcome to the iD OpenStreetMap editor" + text: "Esta es la versión {version} de desarrollo. Para más información visita {website} y reporta cualquier error en {github}." //"This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "en vivo", //"live", + dev: "dev" + }, + + tag_reference: { + description: "Descripción", + on_wiki: "{tag} en wiki.osm.org", //"{tag} on wiki.osm.org" + used_with: "usado con {type}" //"used with {type}" + }, + + validations: { + untagged_point: "Punto sin etiquetar que no es parte de una línea ni zona.", //"Untagged point which is not part of a line or area", + untagged_line: "Línea sin etiquetar", //"Untagged line", + untagged_area: "Zona sin etiquetar", //"Untagged area", + tag_suggests_area: "La etiqueta {tag} sugiere que esta línea debería ser una zona, pero no lo es.", //"The tag {tag} suggests line should be area, but it is not an area", + deprecated_tags: "Etiquetas obsoletas: {tags}" //"Deprecated tags: {tags}" + }, + + zoom: { + in: "Aumentar", // "Zoom In", + out: "Alejar" //"Zoom Out", + } +}; diff --git a/locale/fr.js b/locale/fr.js new file mode 100644 index 000000000..4716080cc --- /dev/null +++ b/locale/fr.js @@ -0,0 +1,227 @@ +locale.fr = { + modes: { + add_area: { + title: "Polygone", + description: "Les polygones peuvent être des parcs, des batîments, des lacs ou tout autre objet surfacique.", + tail: "Cliquez sur la carte pour ajouter un polygone tel qu'un parc, un lac ou un bâtiment." + }, + add_line: { + title: "Ligne", + description: "Les lignes peuvent être des autoroutes, des routes, des chemins ou encore des caneaux.", + tail: "Cliquez sur la carte pour ajouter une nouvelle ligne telle qu'une route ou un nouveau chemin." + }, + add_point: { + title: "Point", + description: "Les points peuvent être des restaurants, des monuments, ou encore des boites aux lettres.", + tail: "Cliquez sur la carte pour ajouter un point tel qu'un restaurant ou un monument." + }, + browse: { + title: "Navigation", + description: "Naviguer ou zoomer sur la carte." + }, + draw_area: { + tail: "Cliquez pour ajouter un point à la zone. Cliquez sur le dernier point pour fermer la zone." + }, + draw_line: { + tail: "Cliquez pour ajouter un point à la ligne. Cliquez sur une autre ligne pour les connecter, puis faîtes un double-clique pour terminer la ligne." + } + }, + + operations: { + add: { + annotation: { + point: "Ajouter un point.", + vertex: "Ajouter un noeud à une ligne." + } + }, + start: { + annotation: { + line: "Commencer une nouvelle ligne.", + area: "Commencer un polygone." + } + }, + 'continue': { + annotation: { + line: "Continuer une ligne.", + area: "Continuer un polygone." + } + }, + cancel_draw: { + annotation: "Annuler un ajout." + }, + change_tags: { + annotation: "Modifier les tags." + }, + circularize: { + title: "Circularize", + description: "Créer un cercle.", + key: "O", + annotation: { + line: "Créer un cercle linéaire.", + area: "Créer un cercle surfacique (disque)." + } + }, + orthogonalize: { + title: "Orthogonaliser", + description: "Rendre une forme orthogonale.", + key: "Q", + annotation: { + line: "Orthogonaliser une ligne orthogonale.", + area: "Orthogonaliser un polygone orthogonale." + } + }, + 'delete': { + title: "Supprimer", + description: "Supprime l'élément de la carte.", + key: "⌫", + annotation: { + point: "Supprime un point.", + vertex: "Supprime le noeud d'une ligne.", + line: "Supprime une ligne.", + area: "Supprime un polygone.", + relation: "Supprime une relation.", + multiple: "Supprime {n} objets." + } + }, + connect: { + annotation: { + point: "Joindre une ligne à un point.", + vertex: "Joindre les noeuds à une ligne.", + line: "Joindre les chemins ensemble.", + area: "Joindre une ligne à un polygone." + } + }, + disconnect: { + title: "Séparer", + description: "Sépare les lignes l'une de l'autre.", + key: "D", + annotation: "Sépare les lignes." + }, + merge: { + title: "Fusionner", + description: "Fusionne les lignes.", + key: "C", + annotation: "Fusionne les {n} ligne." + }, + move: { + title: "Déplacer", + description: "Déplace l'élément à un autre endroit.", + key: "M", + annotation: { + point: "Déplace un point.", + vertex: "Déplace le noeud d'une ligne.", + line: "Déplace une ligne.", + area: "Déplace un polygone." + } + }, + reverse: { + title: "Inverser", + description: "Inverse le sens d'une ligne.", + key: "V", + annotation: "Inverse le sens d'une ligne." + }, + split: { + title: "Couper", + description: "Coupe une ligne en deux par rapport au point sélectionné.", + key: "X", + annotation: "Coupe une ligne." + } + }, + + nothing_to_undo: "Rien à annuler.", + nothing_to_redo: "Rien à refaire.", + + just_edited: "Vous venez de participer à OpenStreetMap!", + browser_notice: "Les navigateurs supportés par cet éditeur sont : Firefox, Chrome, Safari, Opera et Internet Explorer (version 9 et supérieures). Pour éditer la carte, veuillez mettre à jour votre navigateur ou utiliser Potlatch 2.", + view_on_osm: "Consulter dans OSM", + zoom_in_edit: "Zoomer pour modifier la carte", + logout: "Déconnexion", + report_a_bug: "Signaler un bug", + + commit: { + title: "Save Changes", + description_placeholder: "Brief description of your contributions", + upload_explanation: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + save: "Save", + cancel: "Cancel", + warnings: "Warnings", + modified: "Modified", + deleted: "Deleted", + created: "Created" + }, + + contributors: { + list: "Consulter les contributions de {users}", + truncated_list: "Consulter les contributions de {users} et {count} les autres" + }, + + geocoder: { + title: "Trouver un emplacement", + placeholder: "Trouver un endroit", + no_results: "Impossible de localiser l'endroit nommé '{name}'" + }, + + geolocate: { + title: "Show My Location" + }, + + inspector: { + no_documentation_combination: "Aucune documentation n'est disponible pour cette combinaison de tag", + no_documentation_key: "Aucune documentation n'est disponible pour cette clé", + new_tag: "Nouveau tag", + edit_tags: "Editer les tags", + okay: "Okay", + view_on_osm: "View on OSM" + }, + + layerswitcher: { + title: "Fond de carte", + description: "Paramètres du fond de carte", + percent_brightness: "{opacity}% brightness", + fix_misalignment: "Fix misalignment", + reset: "reset" + }, + + restore: { + description: "You have unsaved changes from a previous editing session. Do you wish to restore these changes?", + restore: "Restore", + reset: "Reset" + }, + + save: { + title: "Sauvegarder", + help: "Envoie des modifications au serveyr OpenStreetMap afin qu'elles soient visibles par les autres contributeurs.", + error: "Une erreur est survenue lors de l'enregistrement des données", + uploading: "Envoie des modifications vers OpenStreetMap.", + unsaved_changes: "Vous avez des modifications non enregistrées" + }, + + splash: { + welcome: "Welcome to the iD OpenStreetMap editor", + text: "This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "live", + dev: "dev" + }, + + tag_reference: { + description: "Déscription", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + validations: { + untagged_point: "Point sans aucun tag ne faisant partie ni d'une ligne, ni d'un polygone", + untagged_line: "Ligne sans aucun tag", + untagged_area: "Polygone sans aucun tag", + tag_suggests_area: "Ce tag {tag} suppose que cette ligne devrait être un polygone, or ce n'est pas le cas", + deprecated_tags: "Tags obsolètes : {tags}" + }, + + zoom: { + in: "Zoomer", + out: "Dézoomer" + } +}; diff --git a/locale/ja.js b/locale/ja.js new file mode 100644 index 000000000..3d4abec79 --- /dev/null +++ b/locale/ja.js @@ -0,0 +1,227 @@ +locale.ja = { + modes: { + add_area: { + title: "エリア", + description: "公園や建物、湖沼、をマップに追加します", + tail: "マップをクリックすると、公園や湖沼、建物などのエリアの描画が開始されます。" + }, + add_line: { + title: "ライン", + description: "ラインは車両用の道路や歩道、用水路を表すことができます", + tail: "マップをクリックすると、道路や歩道、流水経路の描画が始まります" + }, + add_point: { + title: "ポイント", + description: "レストランや記念碑、郵便ボックスはポイントで表現します", + tail: "マップをクリックするとポイントを追加できます" + }, + browse: { + title: "ブラウズ", + description: "マップを拡大縮小します" + }, + draw_area: { + tail: "クリックするとエリア上にポイントを追加できます。起点となっているポイントをクリックするとエリアが作成されます" + }, + draw_line: { + tail: "クリックするとライン上にポイントを追加できます。クリックすることで他のラインと接続することが可能です。ライン描画を終了するにはダブルクリックしてください" + } + }, + + operations: { + add: { + annotation: { + point: "ポイントを追加しました", + vertex: "ウェイにノードを追加しました" + } + }, + start: { + annotation: { + line: "ラインの描画を開始しました", + area: "エリアの描画を開始しました" + } + }, + 'continue': { + annotation: { + line: "ライン描画を継続中", + area: "エリア描画を継続中" + } + }, + cancel_draw: { + annotation: "描画をキャンセルしました" + }, + change_tags: { + annotation: "タグを変更しました" + }, + circularize: { + title: "円状に並べる", + description: "この地物を円状に配置します", + key: "O", + annotation: { + line: "ラインを円状にしました", + area: "エリアを円状にしました" + } + }, + orthogonalize: { + title: "角の直交化Orthogonalize", + description: "角を90度に配置します", + key: "Q", + annotation: { + line: "ラインの角を90度にしました", + area: "エリアの角を90度にしました" + } + }, + 'delete': { + title: "削除", + description: "この地物をマップから削除します", + key: "⌫", + annotation: { + point: "ポイント削除しました", + vertex: "ウェイ上のノードを削除しました", + line: "ライン削除しました", + area: "エリア削除しました", + relation: "リレーション削除しました", + multiple: "{n} 個のオブジェクトを削除しました" + } + }, + connect: { + annotation: { + point: "ウェイをポイントに接続しました", + vertex: "ウェイを他のウェイト接続しました", + line: "ウェイとラインを接続しました", + area: "ウェイとエリアを接続しました" + } + }, + disconnect: { + title: "接続解除", + description: "ウェイの接続を解除して切り離します", + key: "D", + annotation: "ウェイの接続を解除しました" + }, + merge: { + title: "結合", + description: "複数のラインを結合します", + key: "C", + annotation: "{n} 本のラインを結合しました" + }, + move: { + title: "移動", + description: "この地物を別の位置に移動させます", + key: "M", + annotation: { + point: "ポイントを移動しました", + vertex: "ウェイ上のノードを移動しました", + line: "ラインを移動しました", + area: "エリアを移動しました" + } + }, + reverse: { + title: "方向反転", + description: "ラインの向きを反転させます", + key: "V", + annotation: "ラインの向きを反転しました" + }, + split: { + title: "分割", + description: "このポイントを境目としてウェイを2つに分割します", + key: "X", + annotation: "ウェイを分割しました" + } + }, + + nothing_to_undo: "やり直す変更点がありません", + nothing_to_redo: "やり直した変更点がありません", + + just_edited: "OpenStreetMap編集完了!", + browser_notice: "このエディタは Firefox, Chrome, Safari, Opera, および Internet Explorer 9 以上をサポートしています。ブラウザのバージョンを更新するか、Potlatch 2を使用して編集してください", + view_on_osm: "OSMで確認", + zoom_in_edit: "編集するにはさらに地図を拡大してください", + logout: "ログアウト", + report_a_bug: "バグを報告", + + commit: { + title: "Save Changes", + description_placeholder: "Brief description of your contributions", + upload_explanation: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + save: "Save", + cancel: "Cancel", + warnings: "Warnings", + modified: "Modified", + deleted: "Deleted", + created: "Created" + }, + + contributors: { + list: "{users} による編集履歴を確認", + truncated_list: "{users} とその他 {count} 人による編集履歴を表示" + }, + + geocoder: { + title: "特定地点を検索", + placeholder: "地点を検索", + no_results: "'{name}' という名称の地点が見つかりません" + }, + + geolocate: { + title: "Show My Location" + }, + + inspector: { + no_documentation_combination: "このタグの組み合わせに関する説明文はありません", + no_documentation_key: "このキーに対する説明文はありません", + new_tag: "新規タグ", + edit_tags: "タグを編集", + okay: "OK", + view_on_osm: "View on OSM" + }, + + layerswitcher: { + title: "背景画像", + description: "背景画像設定", + percent_brightness: "{opacity}% 輝度", + fix_misalignment: "背景画像を移動", + reset: "設定リセット" + }, + + restore: { + description: "You have unsaved changes from a previous editing session. Do you wish to restore these changes?", + restore: "Restore", + reset: "Reset" + }, + + save: { + title: "Save", + help: "変更点をOpenStreetMapに保存し、他ユーザが確認できるようにします", + error: "データ保存中にエラーが発生しました", + uploading: "変更点をOpenStreetMapへアップロードしています", + unsaved_changes: "変更が保存されていません" + }, + + splash: { + welcome: "Welcome to the iD OpenStreetMap editor", + text: "This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "本番サーバ", + dev: "開発サーバ" + }, + + tag_reference: { + description: "説明", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + validations: { + untagged_point: "ポイントにタグが付与されておらず、ラインやエリアの一部でもありません", + untagged_line: "ラインにタグが付与されていません", + untagged_area: "エリアにタグが付与されていません", + tag_suggests_area: "ラインに {tag} タグが付与されています。エリアで描かれるべきです", + deprecated_tags: "タグの重複: {tags}" + }, + + zoom: { + in: "ズームイン", + out: "ズームアウト" + } +}; diff --git a/locale/lv.js b/locale/lv.js new file mode 100644 index 000000000..65018ca29 --- /dev/null +++ b/locale/lv.js @@ -0,0 +1,227 @@ +locale.lv = { + modes: { + add_area: { + title: "Apgabals", + description: "Pievieno parkus, ēkas, ezerus un citus apgabalus.", + tail: "Klikšķiniet uz kartes, lai sāktu zīmēt apgabalu, piemēram, parku, ezeru, vai ēku." + }, + add_line: { + title: "Līnija", + description: "Līnijas var būt ceļi, ielas, takas vai pat kanāli.", + tail: "Klikšķiniet uz kartes, lai sāktu zīmēt līniju, piemēram, ceļu vai taku." + }, + add_point: { + title: "Punkts", + description: "Kafejnīcas, pieminekļi, un veikali var būt punkti.", + tail: "Klikšķiniet uz kartes, lai pievienotu interešu punktu." + }, + browse: { + title: "Pārlūkot", + description: "Pārlūko karti." + }, + draw_area: { + tail: "Klikšķiniet, lai pievinotu mezglus apgabalam. Lai beigtu zīmēt apgabalu, klikšķiniet uz sākuma mezgla." + }, + draw_line: { + tail: "Klikšķiniet, lai pievienotu mezglus līnijai. Lai savienotu ar citām linijām, klikšķiniet uz tām. Dubultklikšķis nobeidz līniju." + } + }, + + operations: { + add: { + annotation: { + point: "Punkts pievienots.", + vertex: "Mezgls pievienots līnijai." + } + }, + start: { + annotation: { + line: "Līnija iesākta.", + area: "Apgabals iesākts." + } + }, + 'continue': { + annotation: { + line: "Līnija turpināta.", + area: "Apgabals turpināts." + } + }, + cancel_draw: { + annotation: "Zīmēšana atcelta." + }, + change_tags: { + annotation: "Apzīmējumi mainīti." + }, + circularize: { + title: "Pārveidot par apļveida", + description: "Pārveidot šo objektu par apļveida.", + key: "O", + annotation: { + line: "Līnija pārveidota par apļveida.", + area: "Apgabals pārveidots par apļveida." + } + }, + orthogonalize: { + title: "Ortogonalizēt", + description: "Pārveidot, lai visi leņķi būtu taisnleņķi.", + key: "Q", + annotation: { + line: "Līnijas leņķi pārvedoti par taisnleņķiem.", + area: "Apgabala leņķi pārvedoti par taisnleņķiem." + } + }, + 'delete': { + title: "Dzēst", + description: "Izdzēst no kartes.", + key: "⌫", + annotation: { + point: "Punkts dzēsts.", + vertex: "Mezgls dzests.", + line: "Līnija dzēsta.", + area: "Apgabals dzēsts.", + relation: "Relācija dzēsta.", + multiple: "{n} objekti dzēsti." + } + }, + connect: { + annotation: { + point: "Līnija savienota ar punktu.", + vertex: "Līnija savienota ar citu.", + line: "Līnija savienota ar līniju.", + area: "Līnija savienota ar apgabalu." + } + }, + disconnect: { + title: "Atvienot", + description: "Atvieno līnijas.", + key: "D", + annotation: "Līnijas atvienotas." + }, + merge: { + title: "Sapludināt", + description: "Sapludināt līnijas.", + key: "C", + annotation: "{n} līnijas sapludinātas." + }, + move: { + title: "Pārvietot", + description: "Pārvieto objektu.", + key: "M", + annotation: { + point: "Punkts pārvietots.", + vertex: "Mezgls pārvietots.", + line: "Līnija pārvietota.", + area: "Apgabals pārvietots." + } + }, + reverse: { + title: "Mainīt virzienu", + description: "Mainīt līnijas virzienu.", + key: "V", + annotation: "Līnijas virziens mainīts." + }, + split: { + title: "Sadalīt", + description: "Sadalīt līniju pie šī punkta.", + key: "X", + annotation: "Līnija sadalīta." + } + }, + + nothing_to_undo: "Nav nekā, ko atcelt", + nothing_to_redo: "Nav nekā, ko atsaukt", + + just_edited: "Jūs nupat rediģējāt OpenStreetMap", + browser_notice: "Šis redaktors tiek atbalstīts ar Firefox, Chrome, Safari, Opera, un Internet Explorer 9 un jaunāku. Lūdzu, atjauniniet savu pārlūkprogrammu vai izmantojiet Potlatch 2 kartes rediģēšanai", + view_on_osm: "Apskatīt OSM lapu", + zoom_in_edit: "pietuviniet, lai labotu karti", + logout: "atslēgties", + report_a_bug: "ziņot par kļūdu", + + commit: { + title: "Saglabāt izmaiņas", + description_placeholder: "Īss apraksts par jūsu ieguldījumu", + upload_explanation: "Izmaiņas, kuras jūs augšupielādējat kā {user}, būs pieejamas visās kartēs, kuras izmanto OpenStreetMap datus.", + save: "Saglabāt", + cancel: "Atcelt", + warnings: "Brīdinājumi", + modified: "Mainīts", + deleted: "Dzēsts", + created: "Izveidots" + }, + + contributors: { + list: "{users} papildinājumi redzami", + truncated_list: "{users} un {count} citu papildinājumi redzami" + }, + + geocoder: { + title: "Atrast vietu", + placeholder: "meklē vietu", + no_results: "Nevar atrast vietu '{name}'" + }, + + geolocate: { + title: "Parādīt manu atrašanās vietu" + }, + + inspector: { + no_documentation_combination: "Šai apzīmējumu kombinācijai nav piejama dokumentācija", + no_documentation_key: "Šai vērtībai nav piejama dokumentācija", + new_tag: "Jauns apzīmējums", + edit_tags: "Labot apzīmējumus", + okay: "Labi", + view_on_osm: "Apskatīt OSM" + }, + + layerswitcher: { + title: "Fons", + description: "Fona iestatījumi", + percent_brightness: "{opacity}% caurspīdīgums", + fix_misalignment: "Labot fona nobīdi", + reset: "Atiestatīt" + }, + + restore: { + description: "Jums ir nesaglabātas izmaiņas no iepriekšējās labošanas sesijas. Vai vēlaties ielādēt šīs izmaiņas?", + restore: "Ielādēt", + reset: "Atmest" + }, + + save: { + title: "Saglabāt", + help: "Saglabā izmaiņas, padarot tās redzamas citiem", + error: "Kļūda. Nevarēja saglabāt izmaiņas", + uploading: "Augšupielādē", + unsaved_changes: "Jums ir nesaglabātas izmaiņas" + }, + + splash: { + welcome: "Laipni lūgti iD OpenStreetMap redaktorā", + text: "Šī ir izstrādes versija {version}. Papildus informācijai skatīt {website} un ziņot par kļūdām {github}." + }, + + source_switch: { + live: "live", + dev: "dev" + }, + + tag_reference: { + description: "Apraksts", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + validations: { + untagged_point: "Neapzīmēts punkts", + untagged_line: "Neapzīmēta līnija", + untagged_area: "Neapzīmēts apgabals", + tag_suggests_area: "Apzīmējums {tag} parasti tiek lietots apgabaliem, bet objekts nav apgabals", + deprecated_tags: "Novecojuši apzīmējumi: {tags}" + }, + + zoom: { + in: "Pietuvināt", + out: "Attālināt" + } +}; diff --git a/locale/tr.js b/locale/tr.js new file mode 100644 index 000000000..402d822f1 --- /dev/null +++ b/locale/tr.js @@ -0,0 +1,227 @@ +locale.tr = { + modes: { + add_area: { + title: "Alan", + description: "Park, bina, göl ve benzeri alanları haritaya ekle.", + tail: "Park, göl ya da bina gibi alanları çizmek için haritaya tıklayın." + }, + add_line: { + title: "Çizgi", + description: "Yollar, sokaklar, patikalar ya da kanallar çizgi ile çizilebilir.", + tail: "Yol, patika yada rota çizmek için haritaya tıklayın." + }, + add_point: { + title: "Nokta", + description: "Restoranlar, anıtlar ya da posta kutuları nokta ile gösterilebilir.", + tail: "Nokta eklemek için haritaya tıklayın." + }, + browse: { + title: "Tara", + description: "Harita üzerinde dolan ve yaklaş." + }, + draw_area: { + tail: "Alanınıza nokta eklemek için tıklayınız. İlk noktaya tıklayarak alan çizimini bitirebilirsiniz." + }, + draw_line: { + tail: "Çizgiye daha fazla nokta eklemek için tıklayınız. Diğer çizgilerle bağlamak için üstlerine tıklyınız ve bitirmek için de son noktada çift tıklayınız." + } + }, + + operations: { + add: { + annotation: { + point: "Nokta eklendi.", + vertex: "Çizgiye bir nod eklendi." + } + }, + start: { + annotation: { + line: "Çizgi çizimi başlatıldı.", + area: "Alan çizimi başlatıldı." + } + }, + 'continue': { + annotation: { + line: "Çizgiye devam edildi.", + area: "Alana devam edildi." + } + }, + cancel_draw: { + annotation: "Çizim iptal edildi." + }, + change_tags: { + annotation: "Etiketler değiştirildi." + }, + circularize: { + title: "Daireleştir", + description: "Yuvarlak hale getir", + key: "O", + annotation: { + line: "Çizgiyi daireleştirin.", + area: "Alanı daireleştirin." + } + }, + orthogonalize: { + title: "Doğrultmak", + description: "Köşeleri doğrultun.", + key: "Q", + annotation: { + line: "Çizginin köşeleri doğrultuldu.", + area: "Alanın köşeleri doğrultuldu." + } + }, + 'delete': { + title: "Sil", + description: "Haritan bunu sil.", + key: "⌫", + annotation: { + point: "Bir nokta silindi.", + vertex: "Yoldan bir nod silindi.", + line: "Bir çizgi silindi.", + area: "Bir alan silindi.", + relation: "Bir ilişki silindi.", + multiple: "{n} adet obje silindi." + } + }, + connect: { + annotation: { + point: "Taraf bir noktaya bağlandı.", + vertex: "Bir taraf diğerine bağlandı.", + line: "Taraf bir çizgiye bağlandı.", + area: "Taraf bir alana bağlandı." + } + }, + disconnect: { + title: "Birbirinden Ayır", + description: "Her iki tarafı da ayır.", + key: "D", + annotation: "Taraflar birbirinden ayrıldı." + }, + merge: { + title: "Birleştir", + description: "Bu çizgileri birleştir.", + key: "C", + annotation: "{n} adet çizgi birleştirildi." + }, + move: { + title: "Taşı", + description: "Bunu farklı bir konuma taşı.", + key: "M", + annotation: { + point: "Bir nokta taşındı.", + vertex: "Yoldan bir nokta taşındı.", + line: "Bir çizgi taşındı.", + area: "Bir alan taşındı." + } + }, + reverse: { + title: "Ters çevir", + description: "Bu çizgiyi ters yönde çevir.", + key: "V", + annotation: "Çizgi ters çevrildi." + }, + split: { + title: "Ayır", + description: "Bu yolu bu noktadan ikiye ayır.", + key: "X", + annotation: "Yolu ayır." + } + }, + + nothing_to_undo: "Geri alınacak birşey yok.", + nothing_to_redo: "Tekrar yapılacak birşey yok.", + + just_edited: "Şu an OpenStreetMap'de bir değişiklik yaptınız!", + browser_notice: "Bu editör sadece Firefox, Chrome, Safari, Opera ile Internet Explorer 9 ve üstü tarayıcılarda çalışmaktadır. Lütfen tarayınıcı güncelleyin ya da Potlatch 2'yi kullanarak haritada güncelleme yapınız.", + view_on_osm: "OSM üstünde Gör", + zoom_in_edit: "Güncelleme yapmak için haritada yakınlaşmalısınız", + logout: "Çıkış", + report_a_bug: "Hata rapor et", + + commit: { + title: "Save Changes", + description_placeholder: "Brief description of your contributions", + upload_explanation: "The changes you upload as {user} will be visible on all maps that use OpenStreetMap data.", + save: "Save", + cancel: "Cancel", + warnings: "Warnings", + modified: "Modified", + deleted: "Deleted", + created: "Created" + }, + + contributors: { + list: "{users} tarafından yapılan katkılar görünmektedir", + truncated_list: "{users} ve diğer {count} tarafından yapılan katkılar görünmektedir" + }, + + geocoder: { + title: "Bir Yer Bul", + placeholder: "bir yer bul", + no_results: "'{name}' ismindeki yer bulunamadı" + }, + + geolocate: { + title: "Show My Location" + }, + + inspector: { + no_documentation_combination: "Bu etiket kombinasyonu için dökümantasyon bulunmamaktadır.", + no_documentation_key: "Bu anahtar için dökümantasyon bulunmamaktadır.", + new_tag: "Yeni Etiket", + edit_tags: "Etiketleri güncelle", + okay: "Tamam", + view_on_osm: "View on OSM" + }, + + layerswitcher: { + title: "Arkaplan", + description: "Arkaplan Ayarları", + percent_brightness: "{opacity}% parlaklık", + fix_misalignment: "Yanlış hizalamayı düzelt", + reset: "Sıfırla" + }, + + restore: { + description: "You have unsaved changes from a previous editing session. Do you wish to restore these changes?", + restore: "Restore", + reset: "Reset" + }, + + save: { + title: "Kaydet", + help: "Diğer kullanıcıların yaptığınız değişiklikleri görmesi için OpenStreetMap'e kaydediniz", + error: "Kaydederken bir hata oluştu", + uploading: "Değişiklikleriniz OpenStreetMap'e gönderiliyor.", + unsaved_changes: "Kaydedilmemiş değişiklikleriniz var" + }, + + splash: { + welcome: "Welcome to the iD OpenStreetMap editor", + text: "This is development version {version}. For more information see {website} and report bugs at {github}." + }, + + source_switch: { + live: "canlı", + dev: "geliştirme" + }, + + tag_reference: { + description: "Açıklama", + on_wiki: "{tag} on wiki.osm.org", + used_with: "used with {type}" + }, + + validations: { + untagged_point: "Herhangi bir çizgi ya da alana bağlantısı olmayan ve etiketlenmemiş bir nokta.", + untagged_line: "Etiketlenmemiş çizgi", + untagged_area: "Etiketlenmemiş alan", + tag_suggests_area: "{tag} etiketi buranın alan olmasını tavsiye ediyor ama alan değil.", + deprecated_tags: "Kullanımdan kaldırılmış etiket : {tags}" + }, + + zoom: { + in: "Yaklaş", + out: "Uzaklaş" + } +}; diff --git a/package.json b/package.json index 5c991f29b..26728933b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "editor", "openstreetmap" ], - "license": "BSD", + "license": "WTFPL", "devDependencies": { "uglify-js": "~2.2.2", "mocha-phantomjs": "~1.1.1" diff --git a/test/bench/node-xml.html b/test/bench/node-xml.html new file mode 100644 index 000000000..174738350 --- /dev/null +++ b/test/bench/node-xml.html @@ -0,0 +1,99 @@ + + + + Node XML + + + +
+
+
+

+        
+    
+    
+
diff --git a/test/index.html b/test/index.html
index c65237b22..05482ab1f 100644
--- a/test/index.html
+++ b/test/index.html
@@ -35,20 +35,23 @@
     
     
 
+    
+    
+    
+
     
     
 
     
     
     
-    
+    
 
     
     
     
     
     
-    
     
     
     
@@ -60,36 +63,48 @@
     
     
     
-    
+    
     
     
     
+    
     
     
     
+    
+    
 
     
-    
-    
-    
-    
+    
+    
+    
+    
+    
+    
+    
+    
     
+    
     
+    
+    
+    
     
     
     
-    
-    
-    
+    
+    
 
     
     
     
-    
     
     
     
+    
     
+    
+    
 
     
     
@@ -102,72 +117,77 @@
     
 
     
-    
+    
+    
     
+    
+    
     
     
     
-    
 
-    
-
-    
-    
-    
-    
-    
-    
+    
+    
+    
+    
+    
+    
+    
 
     
 
-    
+    
+    
+    
+    
+    
+    
+    
+    
+    
 
     
 
     
     
+    
 
-    
-    
-    
+    
+    
+    
+    
+    
+    
     
+    
     
+    
+    
+    
     
     
     
-    
-    
-    
-
-    
+    
+    
 
     
 
-    
-    
-    
-    
-    
-    
-
-    
+    
+    
+    
+    
+    
+    
+    
 
     
-    
     
 
     
     
     
     
-    
+    
     
     
     
@@ -179,9 +199,17 @@
     
 
     
+    
     
     
     
+    
+
+    
+    
+    
+
+    
 
     
 
-    
-
     
 
     
     
+    
+    
 
-    
-    
-    
-    
+    
+    
+    
+    
+    
+    
     
+    
     
+    
+    
+    
     
     
     
-    
-    
-    
-
-    
+    
+    
 
     
 
-    
-    
-    
-    
-    
-    
-
-    
+    
+    
+    
+    
+    
+    
+    
 
     
-    
     
 
     
     
     
     
-    
+    
     
     
     
@@ -78,10 +72,17 @@
     
 
     
+    
     
     
     
 
+    
+    
+    
+
+    
+
     
diff --git a/test/rendering.html b/test/rendering.html
index e5347d0b4..cd75a848b 100644
--- a/test/rendering.html
+++ b/test/rendering.html
@@ -21,18 +21,31 @@
     
     
     
-    
     
     
     
     
 
-    
-    
-    
-    
-    
-    
+    
+    
+    
+    
+    
+    
+
+    
+ + +
+ + @@ -105,6 +118,7 @@ }) .enter() .append('td') + .attr('class', function (d) { return d.mode === 'selected' ? 'mode-select' : 'mode-browse'; }) .append('svg') .attr('width', 200) .attr('height', 30) @@ -115,7 +129,7 @@ graph = iD.Graph([a, b, highway]); d3.select(this) - .attr('class', d.mode === 'selected' ? 'mode-select' : 'mode-browse') + .attr('class', 'behavior-hover') .call(vertices, graph, [a, b], filter) .call(lines, graph, [highway], filter) .call(midpoints, graph, [highway], filter) @@ -123,5 +137,67 @@ .classed(d.mode, d.mode !== 'base'); }); + +
+ + + + + +
BaseSelected
+ + + diff --git a/test/spec/actions/add_entity.js b/test/spec/actions/add_entity.js new file mode 100644 index 000000000..1e2e092ca --- /dev/null +++ b/test/spec/actions/add_entity.js @@ -0,0 +1,7 @@ +describe("iD.actions.AddEntity", function () { + it("adds an entity to the graph", function () { + var entity = iD.Entity(), + graph = iD.actions.AddEntity(entity)(iD.Graph()); + expect(graph.entity(entity.id)).to.equal(entity); + }); +}); diff --git a/test/spec/actions/add_midpoint.js b/test/spec/actions/add_midpoint.js new file mode 100644 index 000000000..1f749217e --- /dev/null +++ b/test/spec/actions/add_midpoint.js @@ -0,0 +1,22 @@ +describe("iD.actions.AddMidpoint", function () { + it("adds the node at the midpoint location", function () { + var node = iD.Node(), + midpoint = {loc: [1, 2], ways: []}, + graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph()); + + expect(graph.entity(node.id).loc).to.eql([1, 2]); + }); + + it("adds the node to all ways at the respective indexes", function () { + var node = iD.Node(), + a = iD.Node(), + b = iD.Node(), + w1 = iD.Way(), + w2 = iD.Way({nodes: [a.id, b.id]}), + midpoint = {loc: [1, 2], ways: [{id: w1.id, index: 0}, {id: w2.id, index: 1}]}, + graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph([a, b, w1, w2])); + + expect(graph.entity(w1.id).nodes).to.eql([node.id]); + expect(graph.entity(w2.id).nodes).to.eql([a.id, node.id, b.id]); + }); +}); diff --git a/test/spec/actions/add_node.js b/test/spec/actions/add_node.js deleted file mode 100644 index ae066edc5..000000000 --- a/test/spec/actions/add_node.js +++ /dev/null @@ -1,7 +0,0 @@ -describe("iD.actions.AddNode", function () { - it("adds a node to the graph", function () { - var node = iD.Node(), - graph = iD.actions.AddNode(node)(iD.Graph()); - expect(graph.entity(node.id)).to.equal(node); - }); -}); diff --git a/test/spec/actions/add_way.js b/test/spec/actions/add_way.js deleted file mode 100644 index 19948c5b2..000000000 --- a/test/spec/actions/add_way.js +++ /dev/null @@ -1,7 +0,0 @@ -describe("iD.actions.AddWay", function () { - it("adds a way to the graph", function () { - var way = iD.Way(), - graph = iD.actions.AddWay(way)(iD.Graph()); - expect(graph.entity(way.id)).to.equal(way); - }); -}); diff --git a/test/spec/actions/change_entity_tags.js b/test/spec/actions/change_tags.js similarity index 57% rename from test/spec/actions/change_entity_tags.js rename to test/spec/actions/change_tags.js index bfa9830c6..0cc198230 100644 --- a/test/spec/actions/change_entity_tags.js +++ b/test/spec/actions/change_tags.js @@ -1,8 +1,8 @@ -describe("iD.actions.ChangeEntityTags", function () { +describe("iD.actions.ChangeTags", function () { it("changes an entity's tags", function () { var entity = iD.Entity(), tags = {foo: 'bar'}, - graph = iD.actions.ChangeEntityTags(entity.id, tags)(iD.Graph([entity])); + graph = iD.actions.ChangeTags(entity.id, tags)(iD.Graph([entity])); expect(graph.entity(entity.id).tags).to.eql(tags); }); }); diff --git a/test/spec/actions/circularize.js b/test/spec/actions/circularize.js new file mode 100644 index 000000000..8215c2253 --- /dev/null +++ b/test/spec/actions/circularize.js @@ -0,0 +1,61 @@ +describe("iD.actions.Circularize", function () { + var projection = d3.geo.mercator(); + + it("creates a circle of 12 nodes", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', loc: [0, 0]}), + 'b': iD.Node({id: 'b', loc: [2, 0]}), + 'c': iD.Node({id: 'c', loc: [2, 2]}), + 'd': iD.Node({id: 'd', loc: [0, 2]}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + }); + + graph = iD.actions.Circularize('-', projection)(graph); + + expect(graph.entity('-').nodes).to.have.length(13); + }); + + it("reuses existing nodes", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', loc: [0, 0]}), + 'b': iD.Node({id: 'b', loc: [2, 0]}), + 'c': iD.Node({id: 'c', loc: [2, 2]}), + 'd': iD.Node({id: 'd', loc: [0, 2]}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + }); + + graph = iD.actions.Circularize('-', projection)(graph); + + expect(graph.entity('-').nodes.slice(0, 4)).to.eql(['c', 'b', 'a', 'd']); + }); + + it("deletes unused nodes that are not members of other ways", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', loc: [0, 0]}), + 'b': iD.Node({id: 'b', loc: [2, 0]}), + 'c': iD.Node({id: 'c', loc: [2, 2]}), + 'd': iD.Node({id: 'd', loc: [0, 2]}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + }); + + graph = iD.actions.Circularize('-', projection, 3)(graph); + + expect(graph.entity('d')).to.be.undefined; + }); + + it("reconnects unused nodes that are members of other ways", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', loc: [0, 0]}), + 'b': iD.Node({id: 'b', loc: [2, 0]}), + 'c': iD.Node({id: 'c', loc: [2, 2]}), + 'd': iD.Node({id: 'd', loc: [0, 2]}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}), + '=': iD.Way({id: '=', nodes: ['d']}) + }); + + graph = iD.actions.Circularize('-', projection, 3)(graph); + + expect(graph.entity('d')).to.be.undefined; + expect(graph.entity('=').nodes).to.eql(['c']); + }); +}); diff --git a/test/spec/actions/connect.js b/test/spec/actions/connect.js new file mode 100644 index 000000000..fc585b43c --- /dev/null +++ b/test/spec/actions/connect.js @@ -0,0 +1,110 @@ +describe("iD.actions.Connect", function() { + describe("#enabled", function () { + it("returns true for two or more nodes", function () { + expect(iD.actions.Connect(['a', 'b']).enabled()).to.be.true; + }); + + it("returns false for less than two nodes", function () { + expect(iD.actions.Connect(['a']).enabled()).to.be.false; + }); + }); + + it("removes all but the final node", function() { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}) + }); + + graph = iD.actions.Connect(['a', 'b', 'c'])(graph); + + expect(graph.entity('a')).to.be.undefined; + expect(graph.entity('b')).to.be.undefined; + expect(graph.entity('c')).not.to.be.undefined; + }); + + it("replaces non-surviving nodes in parent ways", function() { + // a --- b --- c + // + // e + // | + // d + // + // Connect [e, b]. + // + // Expected result: + // + // a --- b --- c + // | + // d + // + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + 'e': iD.Node({id: 'e'}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), + '|': iD.Way({id: '|', nodes: ['d', 'e']}) + }); + + graph = iD.actions.Connect(['e', 'b'])(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('|').nodes).to.eql(['d', 'b']); + }); + + it("handles circular ways", function() { + // c -- a d === e + // | / + // | / + // | / + // b + // + // Connect [a, d]. + // + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + 'e': iD.Node({id: 'e'}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a']}), + '=': iD.Way({id: '=', nodes: ['d', 'e']}) + }); + + graph = iD.actions.Connect(['a', 'd'])(graph); + + expect(graph.entity('-').nodes).to.eql(['d', 'b', 'c', 'd']); + }); + + it("merges tags to the surviving node", function() { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', tags: {a: 'a'}}), + 'b': iD.Node({id: 'b', tags: {b: 'b'}}), + 'c': iD.Node({id: 'c', tags: {c: 'c'}}) + }); + + graph = iD.actions.Connect(['a', 'b', 'c'])(graph); + + expect(graph.entity('c').tags).to.eql({a: 'a', b: 'b', c: 'c'}); + }); + + it("merges memberships to the surviving node", function() { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['c', 'd']}), + 'r1': iD.Relation({id: 'r1', members: [{id: 'b', role: 'r1', type: 'node'}]}), + 'r2': iD.Relation({id: 'r2', members: [{id: 'b', role: 'r2', type: 'node'}, {id: 'c', role: 'r2', type: 'node'}]}) + }); + + graph = iD.actions.Connect(['b', 'c'])(graph); + + expect(graph.entity('r1').members).to.eql([{id: 'c', role: 'r1', type: 'node'}]); + expect(graph.entity('r2').members).to.eql([{id: 'c', role: 'r2', type: 'node'}]); + }); +}); diff --git a/test/spec/actions/delete_multiple.js b/test/spec/actions/delete_multiple.js new file mode 100644 index 000000000..85329b7a8 --- /dev/null +++ b/test/spec/actions/delete_multiple.js @@ -0,0 +1,21 @@ +describe("iD.actions.DeleteMultiple", function () { + it("deletes multiple entities of heterogeneous types", function () { + var n = iD.Node(), + w = iD.Way(), + r = iD.Relation(), + action = iD.actions.DeleteMultiple([n.id, w.id, r.id]), + graph = action(iD.Graph([n, w, r])); + expect(graph.entity(n.id)).to.be.undefined; + expect(graph.entity(w.id)).to.be.undefined; + expect(graph.entity(r.id)).to.be.undefined; + }); + + it("deletes a way and one of its nodes", function () { + var n = iD.Node(), + w = iD.Way({nodes: [n.id]}), + action = iD.actions.DeleteMultiple([w.id, n.id]), + graph = action(iD.Graph([n, w])); + expect(graph.entity(w.id)).to.be.undefined; + expect(graph.entity(n.id)).to.be.undefined; + }); +}); diff --git a/test/spec/actions/delete_relation.js b/test/spec/actions/delete_relation.js new file mode 100644 index 000000000..c96909f3f --- /dev/null +++ b/test/spec/actions/delete_relation.js @@ -0,0 +1,17 @@ +describe("iD.actions.DeleteRelation", function () { + it("removes the relation from the graph", function () { + var relation = iD.Relation(), + action = iD.actions.DeleteRelation(relation.id), + graph = action(iD.Graph([relation])); + expect(graph.entity(relation.id)).to.be.undefined; + }); + + it("removes the relation from parent relations", function () { + var a = iD.Relation(), + b = iD.Relation(), + parent = iD.Relation({members: [{ id: a.id }, { id: b.id }]}), + action = iD.actions.DeleteRelation(a.id), + graph = action(iD.Graph([a, b, parent])); + expect(graph.entity(parent.id).members).to.eql([{ id: b.id }]); + }); +}); diff --git a/test/spec/actions/delete_way.js b/test/spec/actions/delete_way.js index 863b0fa2f..130f5f759 100644 --- a/test/spec/actions/delete_way.js +++ b/test/spec/actions/delete_way.js @@ -1,12 +1,12 @@ -describe("iD.actions.DeleteWay", function () { - it("removes the way from the graph", function () { +describe("iD.actions.DeleteWay", function() { + it("removes the way from the graph", function() { var way = iD.Way(), action = iD.actions.DeleteWay(way.id), graph = iD.Graph([way]).update(action); expect(graph.entity(way.id)).to.be.undefined; }); - it("removes a way from parent relations", function () { + it("removes a way from parent relations", function() { var way = iD.Way(), relation = iD.Relation({members: [{ id: way.id }]}), action = iD.actions.DeleteWay(way.id), @@ -14,7 +14,7 @@ describe("iD.actions.DeleteWay", function () { expect(_.pluck(graph.entity(relation.id).members, 'id')).not.to.contain(way.id); }); - it("deletes member nodes not referenced by another parent", function () { + it("deletes member nodes not referenced by another parent", function() { var node = iD.Node(), way = iD.Way({nodes: [node.id]}), action = iD.actions.DeleteWay(way.id), @@ -22,7 +22,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(node.id)).to.be.undefined; }); - it("does not delete member nodes referenced by another parent", function () { + it("does not delete member nodes referenced by another parent", function() { var node = iD.Node(), way1 = iD.Way({nodes: [node.id]}), way2 = iD.Way({nodes: [node.id]}), @@ -31,8 +31,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(node.id)).not.to.be.undefined; }); - // See #508 - xit("deletes multiple member nodes", function () { + it("deletes multiple member nodes", function() { var a = iD.Node(), b = iD.Node(), way = iD.Way({nodes: [a.id, b.id]}), @@ -42,7 +41,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(b.id)).to.be.undefined; }); - xit("deletes a circular way's start/end node", function () { + it("deletes a circular way's start/end node", function() { var a = iD.Node(), b = iD.Node(), c = iD.Node(), @@ -54,7 +53,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(c.id)).to.be.undefined; }); - it("does not delete member nodes with interesting tags", function () { + it("does not delete member nodes with interesting tags", function() { var node = iD.Node({tags: {highway: 'traffic_signals'}}), way = iD.Way({nodes: [node.id]}), action = iD.actions.DeleteWay(way.id), diff --git a/test/spec/actions/unjoin_node.js b/test/spec/actions/disconnect.js similarity index 89% rename from test/spec/actions/unjoin_node.js rename to test/spec/actions/disconnect.js index 5901e97da..4ce298feb 100644 --- a/test/spec/actions/unjoin_node.js +++ b/test/spec/actions/disconnect.js @@ -1,9 +1,9 @@ -describe("iD.actions.UnjoinNode", function () { +describe("iD.actions.Disconnect", function () { describe("#enabled", function () { it("returns false for a node shared by less than two ways", function () { var graph = iD.Graph({'a': iD.Node()}); - expect(iD.actions.UnjoinNode('a').enabled(graph)).to.equal(false); + expect(iD.actions.Disconnect('a').enabled(graph)).to.equal(false); }); it("returns true for a node shared by two or more ways", function () { @@ -19,7 +19,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - expect(iD.actions.UnjoinNode('b').enabled(graph)).to.equal(true); + expect(iD.actions.Disconnect('b').enabled(graph)).to.equal(true); }); }); @@ -46,7 +46,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.UnjoinNode('b', 'e')(graph); + graph = iD.actions.Disconnect('b', 'e')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); expect(graph.entity('|').nodes).to.eql(['d', 'e']); @@ -64,7 +64,7 @@ describe("iD.actions.UnjoinNode", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.UnjoinNode('b', 'e')(graph); + graph = iD.actions.Disconnect('b', 'e')(graph); // Immutable loc => should be shared by identity. expect(graph.entity('b').loc).to.equal(loc); diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js new file mode 100644 index 000000000..6951013d3 --- /dev/null +++ b/test/spec/actions/join.js @@ -0,0 +1,174 @@ +describe("iD.actions.Join", function () { + describe("#enabled", function () { + it("returns true for ways that share an end/start node", function () { + // a --> b ==> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join(['-', '=']).enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/end node", function () { + // a <-- b <== c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join(['-', '=']).enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/start node", function () { + // a <-- b ==> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join(['-', '=']).enabled(graph)).to.be.true; + }); + + it("returns true for ways that share an end/end node", function () { + // a --> b <== c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join(['-', '=']).enabled(graph)).to.be.true; + }); + + it("returns false for ways that don't share the necessary nodes", function () { + // a -- b -- c + // | + // d + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), + '=': iD.Way({id: '=', nodes: ['b', 'd']}) + }); + + expect(iD.actions.Join(['-', '=']).enabled(graph)).to.be.false; + }); + }); + + it("joins a --> b ==> c", function () { + // Expected result: + // a --> b --> c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b <== c", function () { + // Expected result: + // a <-- b <-- c + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b ==> c", function () { + // Expected result: + // a <-- b <-- c + // tags on === reversed + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c'], tags: {'lanes:forward': 2}}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + expect(graph.entity('-').tags).to.eql({'lanes:backward': 2}); + }); + + it("joins a --> b <== c", function () { + // Expected result: + // a --> b --> c + // tags on === reversed + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['c', 'b'], tags: {'lanes:forward': 2}}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + expect(graph.entity('-').tags).to.eql({'lanes:backward': 2}); + }); + + it("merges tags", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b'], tags: {a: 'a', b: '-', c: 'c'}}), + '=': iD.Way({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('-').tags).to.eql({a: 'a', b: '-; =', c: 'c', d: 'd'}); + }); + + it("merges relations", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['a', 'b']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}), + 'r1': iD.Relation({id: 'r1', members: [{id: '=', role: 'r1', type: 'way'}]}), + 'r2': iD.Relation({id: 'r2', members: [{id: '=', role: 'r2', type: 'way'}, {id: '-', role: 'r2', type: 'way'}]}) + }); + + graph = iD.actions.Join(['-', '='])(graph); + + expect(graph.entity('r1').members).to.eql([{id: '-', role: 'r1', type: 'way'}]); + expect(graph.entity('r2').members).to.eql([{id: '-', role: 'r2', type: 'way'}]); + }); +}); diff --git a/test/spec/actions/merge.js b/test/spec/actions/merge.js new file mode 100644 index 000000000..5ea0b3dea --- /dev/null +++ b/test/spec/actions/merge.js @@ -0,0 +1,20 @@ +describe("iD.actions.Merge", function () { + it("merges multiple points to an area", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', tags: {a: 'a'}}), + 'b': iD.Node({id: 'b', tags: {b: 'b'}}), + 'w': iD.Way({id: 'w', tags: {area: 'yes'}}), + 'r': iD.Relation({id: 'r', members: [{id: 'a', role: 'r', type: 'node'}]}) + }), + action = iD.actions.Merge(['a', 'b', 'w']); + + expect(action.enabled(graph)).to.be.true; + + graph = action(graph); + + expect(graph.entity('a')).to.be.undefined; + expect(graph.entity('b')).to.be.undefined; + expect(graph.entity('w').tags).to.eql({a: 'a', b: 'b', area: 'yes'}); + expect(graph.entity('r').members).to.eql([{id: 'w', role: 'r', type: 'way'}]); + }); +}); diff --git a/test/spec/actions/move_way.js b/test/spec/actions/move_way.js index 38f191a3c..24bc81593 100644 --- a/test/spec/actions/move_way.js +++ b/test/spec/actions/move_way.js @@ -1,5 +1,5 @@ -describe("iD.actions.MoveWay", function () { - it("moves all nodes in a way by the given amount", function () { +describe("iD.actions.MoveWay", function() { + it("moves all nodes in a way by the given amount", function() { var node1 = iD.Node({loc: [0, 0]}), node2 = iD.Node({loc: [5, 10]}), way = iD.Way({nodes: [node1.id, node2.id]}), @@ -14,7 +14,7 @@ describe("iD.actions.MoveWay", function () { expect(loc2[1]).to.be.closeTo( 7.866, 0.001); }); - it("moves repeated nodes only once", function () { + it("moves repeated nodes only once", function() { var node = iD.Node({loc: [0, 0]}), way = iD.Way({nodes: [node.id, node.id]}), delta = [2, 3], diff --git a/test/spec/actions/reverse_way.js b/test/spec/actions/reverse.js similarity index 76% rename from test/spec/actions/reverse_way.js rename to test/spec/actions/reverse.js index ca61cb778..40d0f5500 100644 --- a/test/spec/actions/reverse_way.js +++ b/test/spec/actions/reverse.js @@ -1,9 +1,9 @@ -describe("iD.actions.ReverseWay", function () { +describe("iD.actions.Reverse", function () { it("reverses the order of nodes in the way", function () { var node1 = iD.Node(), node2 = iD.Node(), way = iD.Way({nodes: [node1.id, node2.id]}), - graph = iD.actions.ReverseWay(way.id)(iD.Graph([node1, node2, way])); + graph = iD.actions.Reverse(way.id)(iD.Graph([node1, node2, way])); expect(graph.entity(way.id).nodes).to.eql([node2.id, node1.id]); }); @@ -11,7 +11,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'highway': 'residential'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'highway': 'residential'}); }); @@ -19,7 +19,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'oneway': 'yes'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'oneway': 'yes'}); }); @@ -27,10 +27,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'cycleway:right': 'lane'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'cycleway:left': 'lane'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'cycleway:right': 'lane'}); }); @@ -38,10 +38,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'maxspeed:forward': '25'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:forward': '25'}); }); @@ -49,10 +49,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': 'up'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); }); @@ -60,10 +60,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': 'up'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); }); @@ -71,16 +71,16 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'incline': '5%'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '-5%'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '5%'}); way = iD.Way({tags: {'incline': '.8°'}}); graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'incline': '-.8°'}); }); @@ -88,10 +88,10 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'sidewalk': 'right'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'left'}); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'right'}); }); @@ -99,7 +99,7 @@ describe("iD.actions.ReverseWay", function () { var way = iD.Way({tags: {'maxspeed:forward': '25', 'maxspeed:backward': '30'}}), graph = iD.Graph([way]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25', 'maxspeed:forward': '30'}); }); @@ -108,10 +108,10 @@ describe("iD.actions.ReverseWay", function () { relation = iD.Relation({members: [{type: 'way', id: way.id, role: 'forward'}]}), graph = iD.Graph([way, relation]); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(relation.id).members[0].role).to.eql('backward'); - graph = iD.actions.ReverseWay(way.id)(graph); + graph = iD.actions.Reverse(way.id)(graph); expect(graph.entity(relation.id).members[0].role).to.eql('forward'); }); }); diff --git a/test/spec/actions/split_way.js b/test/spec/actions/split.js similarity index 74% rename from test/spec/actions/split_way.js rename to test/spec/actions/split.js index 2958521a9..2e829fc93 100644 --- a/test/spec/actions/split_way.js +++ b/test/spec/actions/split.js @@ -1,4 +1,4 @@ -describe("iD.actions.SplitWay", function () { +describe("iD.actions.Split", function () { describe("#enabled", function () { it("returns true for a non-end node of a single way", function () { var graph = iD.Graph({ @@ -8,7 +8,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); - expect(iD.actions.SplitWay('b').enabled(graph)).to.be.true; + expect(iD.actions.Split('b').enabled(graph)).to.be.true; }); it("returns false for the first node of a single way", function () { @@ -18,7 +18,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); - expect(iD.actions.SplitWay('a').enabled(graph)).to.be.false; + expect(iD.actions.Split('a').enabled(graph)).to.be.false; }); it("returns false for the last node of a single way", function () { @@ -28,7 +28,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); - expect(iD.actions.SplitWay('b').enabled(graph)).to.be.false; + expect(iD.actions.Split('b').enabled(graph)).to.be.false; }); }); @@ -48,7 +48,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); @@ -63,7 +63,7 @@ describe("iD.actions.SplitWay", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); // Immutable tags => should be shared by identity. expect(graph.entity('-').tags).to.equal(tags); @@ -92,13 +92,73 @@ describe("iD.actions.SplitWay", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); expect(graph.entity('|').nodes).to.eql(['d', 'b']); }); + it("splits a closed way at the given point and its antipode", function () { + // Situation: + // a ---- b + // | | + // d ---- c + // + // Split at a. + // + // Expected result: + // a ---- b + // || | + // d ==== c + // + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) + }); + + var g1 = iD.actions.Split('a', '=')(graph); + expect(g1.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(g1.entity('=').nodes).to.eql(['c', 'd', 'a']); + + var g2 = iD.actions.Split('b', '=')(graph); + expect(g2.entity('-').nodes).to.eql(['b', 'c', 'd']); + expect(g2.entity('=').nodes).to.eql(['d', 'a', 'b']); + + var g3 = iD.actions.Split('c', '=')(graph); + expect(g3.entity('-').nodes).to.eql(['c', 'd', 'a']); + expect(g3.entity('=').nodes).to.eql(['a', 'b', 'c']); + + var g4 = iD.actions.Split('d', '=')(graph); + expect(g4.entity('-').nodes).to.eql(['d', 'a', 'b']); + expect(g4.entity('=').nodes).to.eql(['b', 'c', 'd']); + }); + + it("splits an area by converting it to a multipolygon", function () { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + 'd': iD.Node({id: 'd'}), + '-': iD.Way({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}) + }); + + graph = iD.actions.Split('a', '=')(graph); + expect(graph.entity('-').tags).to.eql({}); + expect(graph.entity('=').tags).to.eql({}); + expect(graph.parentRelations(graph.entity('-'))).to.have.length(1); + + var relation = graph.parentRelations(graph.entity('-'))[0]; + expect(relation.tags).to.eql({type: 'multipolygon', building: 'yes'}); + expect(relation.members).to.eql([ + {id: '-', role: 'outer', type: 'way'}, + {id: '=', role: 'outer', type: 'way'} + ]); + }); + it("adds the new way to parent relations (no connections)", function () { // Situation: // a ---- b ---- c @@ -115,12 +175,15 @@ describe("iD.actions.SplitWay", function () { 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), - 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}]}) + 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); - expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=']); + expect(graph.entity('r').members).to.eql([ + {id: '-', type: 'way', role: 'forward'}, + {id: '=', type: 'way', role: 'forward'} + ]); }); it("adds the new way to parent relations (forward order)", function () { @@ -144,7 +207,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=', '~']); }); @@ -170,7 +233,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '=', '-']); }); @@ -184,7 +247,7 @@ describe("iD.actions.SplitWay", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '-', '=']); }); @@ -214,7 +277,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '=', role: 'from'}, @@ -246,7 +309,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '~', role: 'from'}, @@ -278,7 +341,7 @@ describe("iD.actions.SplitWay", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.SplitWay('b', '=')(graph); + graph = iD.actions.Split('b', '=')(graph); expect(graph.entity('r').members).to.eql([ {id: '-', role: 'from'}, diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js new file mode 100644 index 000000000..c93e42d21 --- /dev/null +++ b/test/spec/behavior/hash.js @@ -0,0 +1,63 @@ +describe("iD.behavior.Hash", function () { + mocha.globals('__onhashchange.hash'); + + var hash, context; + + beforeEach(function () { + context = iD(); + + // Neuter connection + context.connection().loadTiles = function () {}; + + hash = iD.behavior.Hash(context); + + d3.select(document.createElement('div')) + .call(context.map()); + }); + + afterEach(function () { + hash.off(); + }); + + it("sets hadHash if location.hash is present", function () { + location.hash = "map=20.00/38.87952/-77.02405"; + + hash(); + + expect(hash.hadHash).to.be.true; + }); + + it("centerZooms map to requested level", function () { + location.hash = "map=20.00/38.87952/-77.02405"; + + hash(); + + expect(context.map().center()[0]).to.be.closeTo(-77.02405, 0.1); + expect(context.map().center()[1]).to.be.closeTo(38.87952, 0.1); + expect(context.map().zoom()).to.equal(20.0); + }); + + it("centerZooms map at requested coordinates on hash change", function (done) { + hash(); + + d3.select(window).one('hashchange', function () { + expect(context.map().center()[0]).to.be.closeTo(-77.02405, 0.1); + expect(context.map().center()[1]).to.be.closeTo(38.87952, 0.1); + expect(context.map().zoom()).to.equal(20.0); + done(); + }); + + location.hash = "#map=20.00/38.87952/-77.02405"; + }); + + it("stores the current zoom and coordinates in location.hash on map move events", function () { + location.hash = ""; + + hash(); + + context.map().center([38.9, -77.0]); + context.map().zoom(2.0); + + expect(location.hash).to.equal("#map=2.00/-77.0/38.9"); + }); +}); diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js new file mode 100644 index 000000000..ecbb7e7b4 --- /dev/null +++ b/test/spec/behavior/select.js @@ -0,0 +1,69 @@ +describe("iD.behavior.Select", function() { + var a, b, context, behavior, container; + + beforeEach(function() { + container = d3.select('body').append('div'); + + context = iD().container(container); + + a = iD.Node({loc: [0, 0]}); + b = iD.Node({loc: [0, 0]}); + + context.perform(iD.actions.AddEntity(a), iD.actions.AddEntity(b)); + + container.call(context.map()) + .append('div') + .attr('class', 'inspector-wrap'); + + context.surface().selectAll('circle') + .data([a, b]) + .enter().append('circle') + .attr('class', function(d) { return d.id; }); + + context.enter(iD.modes.Browse(context)); + + behavior = iD.behavior.Select(context); + context.install(behavior); + }); + + afterEach(function() { + context.uninstall(behavior); + context.mode().exit(); + container.remove(); + }); + + specify("click on entity selects the entity", function() { + happen.click(context.surface().select('.' + a.id).node()); + expect(context.selection()).to.eql([a.id]); + }); + + specify("click on empty space clears the selection", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().node()); + expect(context.mode().id).to.eql('browse'); + }); + + specify("shift-click on unselected entity adds it to the selection", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id, b.id]); + }); + + specify("shift-click on selected entity removes it from the selection", function() { + context.enter(iD.modes.Select(context, [a.id, b.id])); + happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id]); + }); + + specify("shift-click on last selected entity clears the selection", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().select('.' + a.id).node(), {shiftKey: true}); + expect(context.mode().id).to.eql('browse'); + }); + + specify("shift-click on empty space leaves the selection unchanged", function() { + context.enter(iD.modes.Select(context, [a.id])); + happen.click(context.surface().node(), {shiftKey: true}); + expect(context.selection()).to.eql([a.id]); + }); +}); diff --git a/test/spec/connection.js b/test/spec/connection.js index 2f5d93808..cdc439a46 100644 --- a/test/spec/connection.js +++ b/test/spec/connection.js @@ -2,7 +2,8 @@ describe('iD.Connection', function () { var c; beforeEach(function () { - c = new iD.Connection(); + context = iD(); + c = new iD.Connection(context); }); it('is instantiated', function () { @@ -26,24 +27,24 @@ describe('iD.Connection', function () { c.loadFromURL('data/node.xml', done); }); - it('returns a graph', function (done) { + it('returns an object', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { expect(err).to.not.be.ok; - expect(graph).to.be.instanceOf(iD.Graph); + expect(typeof graph).to.eql('object'); done(); }); }); it('parses a node', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { - expect(graph.entity('n356552551')).to.be.instanceOf(iD.Entity); + expect(graph.n356552551).to.be.instanceOf(iD.Entity); done(); }); }); it('parses a way', function (done) { c.loadFromURL('data/way.xml', function (err, graph) { - expect(graph.entity('w19698713')).to.be.instanceOf(iD.Entity); + expect(graph.w19698713).to.be.instanceOf(iD.Entity); done(); }); }); diff --git a/test/spec/core/difference.js b/test/spec/core/difference.js new file mode 100644 index 000000000..c9647bbea --- /dev/null +++ b/test/spec/core/difference.js @@ -0,0 +1,229 @@ +describe("iD.Difference", function () { + describe("#changes", function () { + it("includes created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); + }); + + it("includes undone created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); + }); + + it("includes modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.update(), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: n1, head: n2}}); + }); + + it("includes undone modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.update(), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: n2, head: n1}}); + }); + + it("includes deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({n: {base: node, head: undefined}}); + }); + + it("includes undone deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(head, base); + expect(diff.changes()).to.eql({n: {base: undefined, head: node}}); + }); + + it("doesn't include created entities that were subsequently deleted", function () { + var node = iD.Node(), + base = iD.Graph(), + head = base.replace(node).remove(node), + diff = iD.Difference(base, head); + expect(diff.changes()).to.eql({}); + }); + }); + + describe("#extantIDs", function () { + it("includes the ids of created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql(['n']); + }); + + it("includes the ids of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql(['n']); + }); + + it("omits the ids of deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.extantIDs()).to.eql([]); + }); + }); + + describe("#created", function () { + it("returns an array of created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.created()).to.eql([node]); + }); + }); + + describe("#modified", function () { + it("returns an array of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.modified()).to.eql([n2]); + }); + }); + + describe("#deleted", function () { + it("returns an array of deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.deleted()).to.eql([node]); + }); + }); + + describe("#complete", function () { + it("includes created entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph(), + head = base.replace(node), + diff = iD.Difference(base, head); + expect(diff.complete()['n']).to.equal(node); + }); + + it("includes modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + base = iD.Graph([n1]), + head = base.replace(n2), + diff = iD.Difference(base, head); + expect(diff.complete()['n']).to.equal(n2); + }); + + it("includes deleted entities", function () { + var node = iD.Node({id: 'n'}), + base = iD.Graph([node]), + head = base.remove(node), + diff = iD.Difference(base, head); + expect(diff.complete()).to.eql({n: undefined}); + }); + + it("includes nodes added to a way", function () { + var n1 = iD.Node({id: 'n1'}), + n2 = iD.Node({id: 'n2'}), + w1 = iD.Way({id: 'w', nodes: ['n1']}), + w2 = w1.addNode('n2'), + base = iD.Graph([n1, n2, w1]), + head = base.replace(w2), + diff = iD.Difference(base, head); + + expect(diff.complete()['n2']).to.equal(n2); + }); + + it("includes nodes removed from a way", function () { + var n1 = iD.Node({id: 'n1'}), + n2 = iD.Node({id: 'n2'}), + w1 = iD.Way({id: 'w', nodes: ['n1', 'n2']}), + w2 = w1.removeNode('n2'), + base = iD.Graph([n1, n2, w1]), + head = base.replace(w2), + diff = iD.Difference(base, head); + + expect(diff.complete()['n2']).to.equal(n2); + }); + + it("includes parent ways of modified nodes", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + way = iD.Way({id: 'w', nodes: ['n']}), + base = iD.Graph([n1, way]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['w']).to.equal(way); + }); + + it("includes parent relations of modified entities", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + rel = iD.Relation({id: 'r', members: [{id: 'n'}]}), + base = iD.Graph([n1, rel]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r']).to.equal(rel); + }); + + it("includes parent relations of modified entities, recursively", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}), + base = iD.Graph([n1, rel1, rel2]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r2']).to.equal(rel2); + }); + + it("includes parent relations of parent ways of modified nodes", function () { + var n1 = iD.Node({id: 'n'}), + n2 = n1.move([1, 2]), + way = iD.Way({id: 'w', nodes: ['n']}), + rel = iD.Relation({id: 'r', members: [{id: 'w'}]}), + base = iD.Graph([n1, way, rel]), + head = base.replace(n2), + diff = iD.Difference(base, head); + + expect(diff.complete()['r']).to.equal(rel); + }); + + it("copes with recursive relations", function () { + var node = iD.Node({id: 'n'}), + rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}, {id: 'r2'}]}), + rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}), + base = iD.Graph([node, rel1, rel2]), + head = base.replace(node.move([1, 2])), + diff = iD.Difference(base, head); + + expect(diff.complete()).to.be.ok; + }); + + it("limits changes to those within a given extent"); + }); +}); diff --git a/test/spec/graph/entity.js b/test/spec/core/entity.js similarity index 76% rename from test/spec/graph/entity.js rename to test/spec/core/entity.js index a573f4c8c..f0b7e0b19 100644 --- a/test/spec/graph/entity.js +++ b/test/spec/core/entity.js @@ -52,12 +52,6 @@ describe('iD.Entity', function () { expect(e.id).to.equal('w1'); }); - it("tags the entity as updated", function () { - var tags = {foo: 'bar'}, - e = iD.Entity().update({tags: tags}); - expect(e._updated).to.to.be.true; - }); - it("doesn't modify the input", function () { var attrs = {tags: {foo: 'bar'}}, e = iD.Entity().update(attrs); @@ -69,6 +63,33 @@ describe('iD.Entity', function () { }); }); + describe("#mergeTags", function () { + it("returns a new Entity", function () { + var a = iD.Entity(), + b = a.mergeTags({}); + expect(b instanceof iD.Entity).to.be.true; + expect(a).not.to.equal(b); + }); + + it("merges tags", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({b: 'b'}); + expect(b.tags).to.eql({a: 'a', b: 'b'}); + }); + + it("combines non-conflicting tags", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({a: 'a'}); + expect(b.tags).to.eql({a: 'a'}); + }); + + it("combines conflicting tags with semicolons", function () { + var a = iD.Entity({tags: {a: 'a'}}), + b = a.mergeTags({a: 'b'}); + expect(b.tags).to.eql({a: 'a; b'}); + }); + }); + describe("#osmId", function () { it("returns an OSM ID as a string", function () { expect(iD.Entity({id: 'w1234'}).osmId()).to.eql('1234'); @@ -77,42 +98,6 @@ describe('iD.Entity', function () { }); }); - describe("#created", function () { - it("returns falsy by default", function () { - expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok; - }); - - it("returns falsy for an unmodified Entity", function () { - expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok; - }); - - it("returns falsy for a modified Entity with positive ID", function () { - expect(iD.Entity({id: 'w1234'}).update({}).created()).not.to.be.ok; - }); - - it("returns truthy for a modified Entity with negative ID", function () { - expect(iD.Entity({id: 'w-1234'}).update({}).created()).to.be.ok; - }); - }); - - describe("#modified", function () { - it("returns falsy by default", function () { - expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok; - }); - - it("returns falsy for an unmodified Entity", function () { - expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok; - }); - - it("returns truthy for a modified Entity with positive ID", function () { - expect(iD.Entity({id: 'w1234'}).update({}).modified()).to.be.ok; - }); - - it("returns falsy for a modified Entity with negative ID", function () { - expect(iD.Entity({id: 'w-1234'}).update({}).modified()).not.to.be.ok; - }); - }); - describe("#intersects", function () { it("returns true for a way with a node within the given extent", function () { var node = iD.Node({loc: [0, 0]}), @@ -129,6 +114,16 @@ describe('iD.Entity', function () { }); }); + describe("#hasDeprecatedTags", function () { + it("returns false if entity has no tags", function () { + expect(iD.Entity().deprecatedTags()).to.eql({}); + }); + + it("returns true if entity has deprecated tags", function () { + expect(iD.Entity({ tags: { barrier: 'wire_fence' } }).deprecatedTags()).to.eql({ barrier: 'wire_fence' }); + }); + }); + describe("#hasInterestingTags", function () { it("returns false if the entity has no tags", function () { expect(iD.Entity().hasInterestingTags()).to.equal(false); diff --git a/test/spec/core/graph.js b/test/spec/core/graph.js new file mode 100644 index 000000000..ef6741bb9 --- /dev/null +++ b/test/spec/core/graph.js @@ -0,0 +1,373 @@ +describe('iD.Graph', function() { + describe("constructor", function () { + it("accepts an entities Object", function () { + var entity = iD.Entity(), + graph = iD.Graph({'n-1': entity}); + expect(graph.entity('n-1')).to.equal(entity); + }); + + it("accepts an entities Array", function () { + var entity = iD.Entity(), + graph = iD.Graph([entity]); + expect(graph.entity(entity.id)).to.equal(entity); + }); + + it("accepts a Graph", function () { + var entity = iD.Entity(), + graph = iD.Graph(iD.Graph([entity])); + expect(graph.entity(entity.id)).to.equal(entity); + }); + + it("copies other's entities", function () { + var entity = iD.Entity(), + base = iD.Graph([entity]), + graph = iD.Graph(base); + expect(graph.entities).not.to.equal(base.entities); + }); + + it("rebases on other's base", function () { + var base = iD.Graph(), + graph = iD.Graph(base); + expect(graph.base().entities).to.equal(base.base().entities); + }); + + it("freezes by default", function () { + expect(iD.Graph().frozen).to.be.true; + }); + + it("remains mutable if passed true as second argument", function () { + expect(iD.Graph([], true).frozen).not.to.be.true; + }); + }); + + describe("#freeze", function () { + it("sets the frozen flag", function () { + expect(iD.Graph([], true).freeze().frozen).to.be.true; + }); + + if (iD.debug) { + it("freezes entities", function () { + expect(Object.isFrozen(iD.Graph().entities)).to.be.true; + }); + } + }); + + describe("#rebase", function () { + it("preserves existing entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph([node]); + graph.rebase({}); + expect(graph.entity('n')).to.equal(node); + }); + + it("includes new entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph(); + graph.rebase({'n': node}); + expect(graph.entity('n')).to.equal(node); + }); + + it("gives precedence to existing entities", function () { + var a = iD.Node({id: 'n'}), + b = iD.Node({id: 'n'}), + graph = iD.Graph([a]); + graph.rebase({'n': b}); + expect(graph.entity('n')).to.equal(a); + }); + + it("inherits entities from base prototypally", function () { + var graph = iD.Graph(); + graph.rebase({'n': iD.Node()}); + expect(graph.entities).not.to.have.ownProperty('n'); + }); + + it("updates parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + graph = iD.Graph([n, w1]); + + graph.rebase({ 'w2': w2 }); + expect(graph.parentWays(n)).to.eql([w1, w2]); + expect(graph._parentWays.hasOwnProperty('n')).to.be.false; + }); + + it("avoids adding duplicate parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]); + graph.rebase({ 'w1': w1 }); + expect(graph.parentWays(n)).to.eql([w1]); + }); + + it("updates parentWays for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + w3 = iD.Way({id: 'w3', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.replace(w2); + + graph.rebase({ 'w3': w3 }); + graph2.rebase({ 'w3': w3 }); + + expect(graph2.parentWays(n)).to.eql([w1, w2, w3]); + }); + + it("avoids re-adding a modified way as a parent way", function() { + var n1 = iD.Node({id: 'n1'}), + n2 = iD.Node({id: 'n2'}), + w1 = iD.Way({id: 'w1', nodes: ['n1', 'n2']}), + w2 = w1.removeNode('n2'), + graph = iD.Graph([n1, n2, w1]), + graph2 = graph.replace(w2); + + graph.rebase({ 'w1': w1 }); + graph2.rebase({ 'w1': w1 }); + + expect(graph2.parentWays(n2)).to.eql([]); + }); + + it("avoids re-adding a deleted way as a parent way", function() { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.remove(w1); + + graph.rebase({ 'w1': w1 }); + graph2.rebase({ 'w1': w1 }); + + expect(graph2.parentWays(n)).to.eql([]); + }); + + it("updates parentRelations", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]); + + graph.rebase({'r2': r2}); + + expect(graph.parentRelations(n)).to.eql([r1, r2]); + expect(graph._parentRels.hasOwnProperty('n')).to.be.false; + }); + + it("avoids re-adding a modified relation as a parent relation", function() { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = r1.removeMember('n'), + graph = iD.Graph([n, r1]), + graph2 = graph.replace(r2); + + graph.rebase({ 'r1': r1 }); + graph2.rebase({ 'r1': r1 }); + + expect(graph2.parentRelations(n)).to.eql([]); + }); + + it("avoids re-adding a deleted relation as a parent relation", function() { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.remove(r1); + + graph.rebase({ 'r1': r1 }); + graph2.rebase({ 'r1': r1 }); + + expect(graph2.parentRelations(n)).to.eql([]); + }); + + it("updates parentRels for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + r3 = iD.Relation({id: 'r3', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.replace(r2); + + graph.rebase({'r3': r3}); + graph2.rebase({'r3': r3}); + expect(graph2.parentRelations(n)).to.eql([r1, r2, r3]); + }); + + }); + + describe("#remove", function () { + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node)).not.to.equal(graph); + }); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.remove(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("removes the entity from the result", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node).entity(node.id)).to.be.undefined; + }); + + it("removes the entity as a parentWay", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("removes the entity as a parentRelation", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n' }]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); + }); + + describe("#replace", function () { + it("is a no-op if the replacement is identical to the existing entity", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.replace(node)).to.equal(graph); + }); + + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.replace(node.update())).not.to.equal(graph); + }); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.replace(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("replaces the entity in the result", function () { + var node1 = iD.Node(), + node2 = node1.update({}), + graph = iD.Graph([node1]); + expect(graph.replace(node2).entity(node2.id)).to.equal(node2); + }); + + it("adds parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("removes parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("doesn't add duplicate parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("adds parentRels", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + + it("removes parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); + + it("doesn't add duplicate parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + }); + + describe("#update", function () { + it("returns a new graph if self is frozen", function () { + var graph = iD.Graph(); + expect(graph.update()).not.to.equal(graph); + }); + + it("returns self if self is not frozen", function () { + var graph = iD.Graph({}, true); + expect(graph.update()).to.equal(graph); + }); + + it("doesn't modify self is self is frozen", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + + graph.update(function (graph) { graph.remove(node); }); + + expect(graph.entity(node.id)).to.equal(node); + }); + + it("modifies self is self is not frozen", function () { + var node = iD.Node(), + graph = iD.Graph([node], true); + + graph.update(function (graph) { graph.remove(node); }); + + expect(graph.entity(node.id)).to.be.undefined; + }); + + it("executes all of the given functions", function () { + var a = iD.Node(), + b = iD.Node(), + graph = iD.Graph([a]); + + graph = graph.update( + function (graph) { graph.remove(a); }, + function (graph) { graph.replace(b); } + ); + + expect(graph.entity(a.id)).to.be.undefined; + expect(graph.entity(b.id)).to.equal(b); + }); + }); + + describe("#parentWays", function() { + it("returns an array of ways that contain the given node id", function () { + var node = iD.Node({id: "n1"}), + way = iD.Way({id: "w1", nodes: ["n1"]}), + graph = iD.Graph({n1: node, w1: way}); + expect(graph.parentWays(node)).to.eql([way]); + expect(graph.parentWays(way)).to.eql([]); + }); + }); + + describe("#parentRelations", function() { + it("returns an array of relations that contain the given entity id", function () { + var node = iD.Node({id: "n1"}), + nonnode = iD.Node({id: "n2"}), + relation = iD.Relation({id: "r1", members: [{ id: "n1", role: 'from' }]}), + graph = iD.Graph({n1: node, r1: relation}); + expect(graph.parentRelations(node)).to.eql([relation]); + expect(graph.parentRelations(nonnode)).to.eql([]); + }); + }); + + describe("#childNodes", function () { + it("returns an array of child nodes", function () { + var node = iD.Node({id: "n1"}), + way = iD.Way({id: "w1", nodes: ["n1"]}), + graph = iD.Graph({n1: node, w1: way}); + expect(graph.childNodes(way)).to.eql([node]); + }); + }); +}); diff --git a/test/spec/graph/history.js b/test/spec/core/history.js similarity index 64% rename from test/spec/graph/history.js rename to test/spec/core/history.js index b59190c0f..0618f3ec3 100644 --- a/test/spec/graph/history.js +++ b/test/spec/core/history.js @@ -1,10 +1,13 @@ describe("iD.History", function () { - var history, spy, + var context, history, spy, action = function() { return iD.Graph(); }; beforeEach(function () { - history = iD.History(); + context = iD(); + history = context.history(); spy = sinon.spy(); + // clear lock + context.storage(history._getKey('lock'), null); }); describe("#graph", function () { @@ -13,7 +16,25 @@ describe("iD.History", function () { }); }); + describe("#merge", function () { + it("merges the entities into all graph versions", function () { + var n = iD.Node({id: 'n'}); + history.merge({n: n}); + expect(history.graph().entity('n')).to.equal(n); + }); + + it("emits a change event", function () { + history.on('change', spy); + history.merge({}); + expect(spy).to.have.been.called; + }); + }); + describe("#perform", function () { + it("returns a difference", function () { + expect(history.perform(action).changes()).to.eql({}); + }); + it("updates the graph", function () { var node = iD.Node(); history.perform(function (graph) { return graph.replace(node); }); @@ -27,8 +48,8 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); - history.perform(action); - expect(spy).to.have.been.calledWith([]); + var difference = history.perform(action); + expect(spy).to.have.been.calledWith(difference); }); it("performs multiple actions", function () { @@ -42,6 +63,10 @@ describe("iD.History", function () { }); describe("#replace", function () { + it("returns a difference", function () { + expect(history.replace(action).changes()).to.eql({}); + }); + it("updates the graph", function () { var node = iD.Node(); history.replace(function (graph) { return graph.replace(node); }); @@ -56,8 +81,8 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); - history.replace(action); - expect(spy).to.have.been.calledWith([]); + var difference = history.replace(action); + expect(spy).to.have.been.calledWith(difference); }); it("performs multiple actions", function () { @@ -71,6 +96,11 @@ describe("iD.History", function () { }); describe("#pop", function () { + it("returns a difference", function () { + history.perform(action, "annotation"); + expect(history.pop().changes()).to.eql({}); + }); + it("updates the graph", function () { history.perform(action, "annotation"); history.pop(); @@ -86,12 +116,16 @@ describe("iD.History", function () { it("emits a change event", function () { history.perform(action); history.on('change', spy); - history.pop(); - expect(spy).to.have.been.calledWith([]); + var difference = history.pop(); + expect(spy).to.have.been.calledWith(difference); }); }); describe("#undo", function () { + it("returns a difference", function () { + expect(history.undo().changes()).to.eql({}); + }); + it("pops the undo stack", function () { history.perform(action, "annotation"); history.undo(); @@ -121,12 +155,16 @@ describe("iD.History", function () { it("emits a change event", function () { history.perform(action); history.on('change', spy); - history.undo(); - expect(spy).to.have.been.calledWith([]); + var difference = history.undo(); + expect(spy).to.have.been.calledWith(difference); }); }); describe("#redo", function () { + it("returns a difference", function () { + expect(history.redo().changes()).to.eql({}); + }); + it("emits an redone event", function () { history.perform(action); history.undo(); @@ -139,8 +177,8 @@ describe("iD.History", function () { history.perform(action); history.undo(); history.on('change', spy); - history.redo(); - expect(spy).to.have.been.calledWith([]); + var difference = history.redo(); + expect(spy).to.have.been.calledWith(difference); }); }); @@ -153,17 +191,15 @@ describe("iD.History", function () { it("includes modified entities", function () { var node1 = iD.Node({id: "n1"}), - node2 = node1.update({}), - graph = iD.Graph([node1]); - history.merge(graph); + node2 = node1.update({}); + history.merge({ n1: node1}); history.perform(function (graph) { return graph.replace(node2); }); expect(history.changes().modified).to.eql([node2]); }); it("includes deleted entities", function () { - var node = iD.Node({id: "n1"}), - graph = iD.Graph([node]); - history.merge(graph); + var node = iD.Node({id: "n1"}); + history.merge({ n1: node }); history.perform(function (graph) { return graph.remove(node); }); expect(history.changes().deleted).to.eql([node]); }); @@ -189,7 +225,7 @@ describe("iD.History", function () { it("is the sum of all types of changes", function() { var node1 = iD.Node({id: "n1"}), node2 = iD.Node(); - history.merge(iD.Graph([node1])); + history.merge({ n1: node1 }); history.perform(function (graph) { return graph.remove(node1); }); expect(history.numChanges()).to.eql(1); history.perform(function (graph) { return graph.replace(node2); }); @@ -213,4 +249,51 @@ describe("iD.History", function () { expect(spy).to.have.been.called; }); }); + + describe("#lock", function() { + it("acquires lock if possible", function() { + expect(history.lock()).to.be.true; + expect(history.lock()).to.be.false; + }); + }); + + describe("#save", function() { + + it("doesn't do anything if it doesn't have the lock", function() { + var key = history._getKey('history'); + context.storage(key, null); + history.save(); + expect(context.storage(key)).to.be.undefined; + context.storage(key, 'something'); + expect(context.storage(key)).to.equal('something'); + history.save(); + context.storage(key, null); + }); + + it("saves to localStorage", function() { + var node = iD.Node({ id: 'n' }); + history.lock(); + history.perform(iD.actions.AddEntity(node)); + history.save(); + var saved = JSON.parse(context.storage(history._getKey('history'))); + expect(saved[1].entities.n.id).to.eql('n'); + }); + }); + + describe("#load", function() { + it("saves and loads a created and deleted entities", function() { + var node = iD.Node({ id: 'n' }), + node2 = iD.Node({ id: 'n2' }); + history.lock(); + history.perform(iD.actions.AddEntity(node)); + history.perform(iD.actions.AddEntity(node2)); + history.perform(iD.actions.DeleteNode('n2')); + history.save(); + history.reset(); + expect(history.graph().entity('n')).to.be.undefined + history.load(); + expect(history.graph().entity('n').id).to.equal('n'); + expect(history.graph().entity('n2')).to.be.undefined; + }); + }); }); diff --git a/test/spec/graph/node.js b/test/spec/core/node.js similarity index 88% rename from test/spec/graph/node.js rename to test/spec/core/node.js index ad149261e..71a2c5e95 100644 --- a/test/spec/graph/node.js +++ b/test/spec/core/node.js @@ -4,15 +4,6 @@ describe('iD.Node', function () { expect(iD.Node().type).to.equal("node"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Node().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Node({id: 'n1234'}).created()).not.to.be.ok; - expect(iD.Node({id: 'n1234'}).modified()).not.to.be.ok; - }); - it("defaults tags to an empty object", function () { expect(iD.Node().tags).to.eql({}); }); diff --git a/test/spec/graph/relation.js b/test/spec/core/relation.js similarity index 69% rename from test/spec/graph/relation.js rename to test/spec/core/relation.js index 8edf9c1e7..39b748e0a 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/core/relation.js @@ -10,15 +10,6 @@ describe('iD.Relation', function () { expect(iD.Relation().type).to.equal("relation"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Relation().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Relation({id: 'r1234'}).created()).not.to.be.ok; - expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok; - }); - it("defaults members to an empty array", function () { expect(iD.Relation().members).to.eql([]); }); @@ -140,6 +131,35 @@ describe('iD.Relation', function () { }); }); + describe("#replaceMember", function () { + it("returns self if self does not contain needle", function () { + var r = iD.Relation({members: []}); + expect(r.replaceMember({id: 'a'}, {id: 'b'})).to.equal(r); + }); + + it("replaces a member which doesn't already exist", function () { + var r = iD.Relation({members: [{id: 'a', role: 'a'}]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'a', type: 'node'}]); + }); + + it("preserves the existing role", function () { + var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'a', type: 'node'}]); + }); + + it("uses the replacement type", function () { + var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'way'}).members).to.eql([{id: 'b', role: 'a', type: 'way'}]); + }); + + it("removes members if replacing them would produce duplicates", function () { + var r = iD.Relation({members: [ + {id: 'a', role: 'b', type: 'node'}, + {id: 'b', role: 'b', type: 'node'}]}); + expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members).to.eql([{id: 'b', role: 'b', type: 'node'}]); + }); + }); + describe("#asJXON", function () { it('converts a relation to jxon', function() { var relation = iD.Relation({id: 'r-1', members: [{id: 'w1', role: 'forward', type: 'way'}], tags: {type: 'route'}}); @@ -155,107 +175,135 @@ describe('iD.Relation', function () { }); }); + describe("#asGeoJSON", function (){ + it('converts a multipolygon to a GeoJSON MultiPolygon feature', function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + g = iD.Graph([a, b, c, w, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({type: 'multipolygon'}); + expect(json.geometry.type).to.equal('MultiPolygon'); + expect(json.geometry.coordinates).to.eql([[[[1, 1], [2, 2], [3, 3], [1, 1]]]]); + }); + + it('converts a relation to a GeoJSON FeatureCollection', function() { + var a = iD.Node({loc: [1, 1]}), + r = iD.Relation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]}), + g = iD.Graph([a, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('FeatureCollection'); + expect(json.properties).to.eql({type: 'type'}); + expect(json.features).to.eql([_.extend({role: 'role'}, a.asGeoJSON(g))]); + }); + }); + describe("#multipolygon", function () { specify("single polygon consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("single polygon consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id, a.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("single polygon consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [a.id, d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("multiple polygons consisting of single ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), - e = iD.Node(), - f = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), + e = iD.Node({loc: [5, 5]}), + f = iD.Node({loc: [6, 6]}), w1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), w2 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]], [[d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]], [[d.loc, e.loc, f.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, alternate order", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [c.id, d.id]}), w2 = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order", function () { @@ -268,7 +316,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("single polygon with single single-way inner", function () { @@ -283,7 +331,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, outer, inner, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with single multi-way inner", function () { @@ -302,7 +350,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with multiple single-way inners", function () { @@ -324,7 +372,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d], [g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc], [g.loc, h.loc, i.loc, g.loc]]]); }); specify("multiple polygons with single single-way inner", function () { @@ -346,30 +394,30 @@ describe('iD.Relation', function () { {id: inner.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]], [[g.loc, h.loc, i.loc, g.loc]]]); }); specify("invalid geometry: unmatched inner", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("incomplete relation", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way(), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, w1, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); }); }); diff --git a/test/spec/graph/way.js b/test/spec/core/way.js similarity index 85% rename from test/spec/graph/way.js rename to test/spec/core/way.js index ec41edec6..2201b7fc4 100644 --- a/test/spec/graph/way.js +++ b/test/spec/core/way.js @@ -10,15 +10,6 @@ describe('iD.Way', function() { expect(iD.Way().type).to.equal("way"); }); - it("returns a created Entity if no ID is specified", function () { - expect(iD.Way().created()).to.be.ok; - }); - - it("returns an unmodified Entity if ID is specified", function () { - expect(iD.Way({id: 'w1234'}).created()).not.to.be.ok; - expect(iD.Way({id: 'w1234'}).modified()).not.to.be.ok; - }); - it("defaults nodes to an empty array", function () { expect(iD.Way().nodes).to.eql([]); }); @@ -104,16 +95,20 @@ describe('iD.Way', function() { expect(iD.Way({tags: { area: 'yes' }}).isArea()).to.equal(true); }); - it('returns true if the way is closed and has no tags', function() { - expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(true); + it('returns false if the way is closed and has no tags', function() { + expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(false); + }); + + it('returns true if the way is closed and has a key in iD.Way.areaKeys', function() { + expect(iD.Way({nodes: ['n1', 'n1'], tags: {building: 'yes'}}).isArea()).to.equal(true); + }); + + it('returns false if the way is closed and has no keys in iD.Way.areaKeys', function() { + expect(iD.Way({nodes: ['n1', 'n1'], tags: {a: 'b'}}).isArea()).to.equal(false); }); it('returns false if the way is closed and has tag area=no', function() { - expect(iD.Way({tags: { area: 'no' }, nodes: ['n1', 'n1']}).isArea()).to.equal(false); - }); - - it('returns false if the way is closed and has highway tag', function() { - expect(iD.Way({tags: { highway: 'residential' }, nodes: ['n1', 'n1']}).isArea()).to.equal(false); + expect(iD.Way({nodes: ['n1', 'n1'], tags: {area: 'no', building: 'yes'}}).isArea()).to.equal(false); }); }); @@ -216,7 +211,7 @@ describe('iD.Way', function() { }); describe("#asGeoJSON", function () { - it("converts to a GeoJSON LineString features", function () { + it("converts a line to a GeoJSON LineString features", function () { var a = iD.Node({loc: [1, 2]}), b = iD.Node({loc: [3, 4]}), w = iD.Way({tags: {highway: 'residential'}, nodes: [a.id, b.id]}), @@ -228,5 +223,19 @@ describe('iD.Way', function() { expect(json.geometry.type).to.equal('LineString'); expect(json.geometry.coordinates).to.eql([[1, 2], [3, 4]]); }); + + it("converts an area to a GeoJSON Polygon features", function () { + var a = iD.Node({loc: [1, 2]}), + b = iD.Node({loc: [3, 4]}), + c = iD.Node({loc: [5, 6]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + graph = iD.Graph([a, b, c, w]), + json = w.asGeoJSON(graph); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({area: 'yes'}); + expect(json.geometry.type).to.equal('Polygon'); + expect(json.geometry.coordinates).to.eql([[[1, 2], [3, 4], [5, 6], [1, 2]]]); + }); }); }); diff --git a/test/spec/geo.js b/test/spec/geo.js new file mode 100644 index 000000000..113d4795d --- /dev/null +++ b/test/spec/geo.js @@ -0,0 +1,96 @@ +describe('iD.geo', function() { + describe('.roundCoords', function() { + expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]); + }); + + describe('.interp', function() { + it('interpolates halfway', function() { + var a = [0, 0], + b = [10, 10]; + expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]); + }); + it('interpolates to one side', function() { + var a = [0, 0], + b = [10, 10]; + expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]); + }); + }); + + describe('.dist', function() { + it('distance between two same points is zero', function() { + var a = [0, 0], + b = [0, 0]; + expect(iD.geo.dist(a, b)).to.eql(0); + }); + it('a straight 10 unit line is 10', function() { + var a = [0, 0], + b = [10, 0]; + expect(iD.geo.dist(a, b)).to.eql(10); + }); + it('a pythagorean triangle is right', function() { + var a = [0, 0], + b = [4, 3]; + expect(iD.geo.dist(a, b)).to.eql(5); + }); + }); + + describe('.pointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.geo.pointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [ + [0, 0], + [0, 1], + [1, 1], + [1, 0], + [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geo.pointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('.polygonContainsPolygon', function() { + it('says a polygon in a polygon is in', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('.polygonIntersectsPolygon', function() { + it('says a polygon in a polygon intersects it', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('says a polygon that partially intersects does', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('says totally disjoint polygons do not intersect', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; + expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('.pathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geo.pathLength(path)).to.eql(6); + }); + }); +}); diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js deleted file mode 100644 index 25382e7c9..000000000 --- a/test/spec/graph/graph.js +++ /dev/null @@ -1,192 +0,0 @@ -describe('iD.Graph', function() { - it("can be constructed with an entities Object", function () { - var entity = iD.Entity(), - graph = iD.Graph({'n-1': entity}); - expect(graph.entity('n-1')).to.equal(entity); - }); - - it("can be constructed with an entities Array", function () { - var entity = iD.Entity(), - graph = iD.Graph([entity]); - expect(graph.entity(entity.id)).to.equal(entity); - }); - - if (iD.debug) { - it("is frozen", function () { - expect(Object.isFrozen(iD.Graph())).to.be.true; - }); - - it("freezes entities", function () { - expect(Object.isFrozen(iD.Graph().entities)).to.be.true; - }); - } - - describe("#remove", function () { - it("returns a new graph", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - expect(graph.remove(node)).not.to.equal(graph); - }); - - it("doesn't modify the receiver", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - graph.remove(node); - expect(graph.entity(node.id)).to.equal(node); - }); - - it("removes the entity from the result", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - expect(graph.remove(node).entity(node.id)).to.be.undefined; - }); - }); - - describe("#replace", function () { - it("returns a new graph", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - expect(graph.replace(node)).not.to.equal(graph); - }); - - it("doesn't modify the receiver", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - graph.replace(node); - expect(graph.entity(node.id)).to.equal(node); - }); - - it("replaces the entity in the result", function () { - var node1 = iD.Node(), - node2 = node1.update({}), - graph = iD.Graph([node1]); - expect(graph.replace(node2).entity(node2.id)).to.equal(node2); - }); - }); - - describe("#update", function () { - it("returns a new graph if self is frozen", function () { - var graph = iD.Graph(); - expect(graph.update()).not.to.equal(graph); - }); - - it("returns self if self is not frozen", function () { - var graph = iD.Graph({}, true); - expect(graph.update()).to.equal(graph); - }); - - it("doesn't modify self is self is frozen", function () { - var node = iD.Node(), - graph = iD.Graph([node]); - - graph.update(function (graph) { graph.remove(node); }); - - expect(graph.entity(node.id)).to.equal(node); - }); - - it("modifies self is self is not frozen", function () { - var node = iD.Node(), - graph = iD.Graph([node], true); - - graph.update(function (graph) { graph.remove(node); }); - - expect(graph.entity(node.id)).to.be.undefined; - }); - - it("executes all of the given functions", function () { - var a = iD.Node(), - b = iD.Node(), - graph = iD.Graph([a]); - - graph = graph.update( - function (graph) { graph.remove(a); }, - function (graph) { graph.replace(b); } - ); - - expect(graph.entity(a.id)).to.be.undefined; - expect(graph.entity(b.id)).to.equal(b); - }); - }); - - describe("#parentWays", function() { - it("returns an array of ways that contain the given node id", function () { - var node = iD.Node({id: "n1"}), - way = iD.Way({id: "w1", nodes: ["n1"]}), - graph = iD.Graph({n1: node, w1: way}); - expect(graph.parentWays(node)).to.eql([way]); - expect(graph.parentWays(way)).to.eql([]); - }); - }); - - describe("#parentRelations", function() { - it("returns an array of relations that contain the given entity id", function () { - var node = iD.Node({id: "n1"}), - nonnode = iD.Node({id: "n2"}), - relation = iD.Relation({id: "r1", members: [{ id: "n1", role: 'from' }]}), - graph = iD.Graph({n1: node, r1: relation}); - expect(graph.parentRelations(node)).to.eql([relation]); - expect(graph.parentRelations(nonnode)).to.eql([]); - }); - }); - - describe("#childNodes", function () { - it("returns an array of child nodes", function () { - var node = iD.Node({id: "n1"}), - way = iD.Way({id: "w1", nodes: ["n1"]}), - graph = iD.Graph({n1: node, w1: way}); - expect(graph.childNodes(way)).to.eql([node]); - }); - }); - - describe("#difference", function () { - it("returns an Array of ids of changed entities", function () { - var initial = iD.Node({id: "n1"}), - updated = initial.update({}), - created = iD.Node(), - deleted = iD.Node({id: 'n2'}), - graph1 = iD.Graph([initial, deleted]), - graph2 = graph1.replace(updated).replace(created).remove(deleted); - expect(graph2.difference(graph1)).to.eql([created.id, updated.id, deleted.id]); - }); - - it("includes created entities that were subsequently deleted", function () { - var node = iD.Node(), - graph1 = iD.Graph([node]), - graph2 = graph1.remove(node); - expect(graph2.difference(graph1)).to.eql([node.id]); - }); - }); - - describe("#modified", function () { - it("returns an Array of ids of modified entities", function () { - var node1 = iD.Node({id: 'n1', _updated: true}), - node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); - expect(graph.modified()).to.eql([node1.id]); - }); - }); - - describe("#created", function () { - it("returns an Array of ids of created entities", function () { - var node1 = iD.Node({id: 'n-1', _updated: true}), - node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); - expect(graph.created()).to.eql([node1.id]); - }); - }); - - describe("#deleted", function () { - it("returns an Array of ids of deleted entities", function () { - var node1 = iD.Node({id: "n1"}), - node2 = iD.Node(), - graph = iD.Graph([node1, node2]).remove(node1); - expect(graph.deleted()).to.eql([node1.id]); - }); - - it("doesn't include created entities that were subsequently deleted", function () { - var node = iD.Node(), - graph = iD.Graph([node]).remove(node); - expect(graph.deleted()).to.eql([]); - }); - }); -}); diff --git a/test/spec/lib/locale.js b/test/spec/lib/locale.js new file mode 100644 index 000000000..3228aa99b --- /dev/null +++ b/test/spec/lib/locale.js @@ -0,0 +1,26 @@ +describe("locale", function() { + var saved, error; + + beforeEach(function() { + saved = locale; + error = console.error; + console.error = function () {}; + locale = { _current: 'en', en: {test: 'test', foo: 'bar'}, __: {}} + }); + + afterEach(function() { + locale = saved; + console.error = error; + }); + + describe("t", function() { + it("defaults to locale._current", function() { + expect(t('test')).to.equal('test'); + }); + + it("falls back to en", function() { + locale._current = '__'; + expect(t('test')).to.equal('test'); + }); + }); +}); diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index 68fe80403..d3518ead9 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -1,41 +1,43 @@ -describe("iD.modes.AddPoint", function () { - var container, map, history, controller, mode; +describe("iD.modes.AddPoint", function() { + var context; - beforeEach(function () { - container = d3.select('body').append('div'); - history = iD.History(); - map = iD.Map().history(history); - controller = iD.Controller(map, history); + beforeEach(function() { + var container = d3.select(document.createElement('div')); - container.call(map); - container.append('div') + context = iD() + .container(container); + + container.call(context.map()) + .append('div') .attr('class', 'inspector-wrap'); - mode = iD.modes.AddPoint(); - controller.enter(mode); - }); - - afterEach(function() { - container.remove(); + context.enter(iD.modes.AddPoint(context)); }); describe("clicking the map", function () { - it("adds a node", function () { - happen.click(map.surface.node(), {}); - expect(history.changes().created).to.have.length(1); + it("adds a node", function() { + happen.mousedown(context.surface().node(), {}); + happen.mouseup(window, {}); + expect(context.changes().created).to.have.length(1); + context.mode().exit(); }); - it("selects the node", function () { - happen.click(map.surface.node(), {}); - expect(controller.mode.id).to.equal('select'); - expect(controller.mode.entity).to.equal(history.changes().created[0]); + it("selects the node", function() { + happen.mousedown(context.surface().node(), {}); + happen.mouseup(window, {}); + expect(context.mode().id).to.equal('select'); + expect(context.mode().selection()).to.eql([context.changes().created[0].id]); + context.mode().exit(); }); }); - describe("pressing ⎋", function () { - it("exits to browse mode", function () { + describe("pressing ⎋", function() { + it("exits to browse mode", function(done) { happen.keydown(document, {keyCode: 27}); - expect(controller.mode.id).to.equal('browse'); + window.setTimeout(function() { + expect(context.mode().id).to.equal('browse'); + done(); + }, 200); }); }); }); diff --git a/test/spec/oauth.js b/test/spec/oauth.js index 8fd57c18c..b7ae1074f 100644 --- a/test/spec/oauth.js +++ b/test/spec/oauth.js @@ -2,7 +2,8 @@ describe('iD.OAuth', function() { var o; beforeEach(function() { - o = iD.OAuth(); + context = iD(); + o = iD.OAuth(context); }); describe('#logout', function() { diff --git a/test/spec/renderer/background.js b/test/spec/renderer/background.js index 6ba871d11..cdfb3239d 100644 --- a/test/spec/renderer/background.js +++ b/test/spec/renderer/background.js @@ -22,32 +22,26 @@ describe('iD.Background', function() { }); it('#source', function() { - expect(c.source(iD.BackgroundSource.Bing)).to.equal(c); - expect(c.source()).to.equal(iD.BackgroundSource.Bing); - }); - }); - - describe('iD.BackgroundSource.Bing', function() { - it('generates tiles', function() { - expect(iD.BackgroundSource.Bing([0,0,0])).to.equal('http://ecn.t0.tiles.virtualearth.net/tiles/a.jpeg?g=587&mkt=en-gb&n=z'); + expect(c.source(iD.layers[0])).to.equal(c); + expect(c.source()).to.equal(iD.layers[0]); }); }); describe('iD.BackgroundSource.Template', function() { it('does not error with blank template', function() { - var source = iD.BackgroundSource.template(''); + var source = iD.BackgroundSource.template({ template: '' }); expect(source([0,1,2])).to.equal(''); }); it('generates a tile-generating source', function() { - var source = iD.BackgroundSource.template('{z}/{x}/{y}'); + var source = iD.BackgroundSource.template({ template: '{z}/{x}/{y}' }); expect(source([0,1,2])).to.equal('2/0/1'); }); it('supports subdomains', function() { - var source = iD.BackgroundSource.template('{t}/{z}/{x}/{y}', ['apples', 'oranges']); + var source = iD.BackgroundSource.template({ template: '{t}/{z}/{x}/{y}', subdomains: ['apples', 'oranges'] }); expect(source([0,1,2])).to.equal('apples/2/0/1'); }); it('distributes requests between subdomains', function() { - var source = iD.BackgroundSource.template('{t}/{z}/{x}/{y}', ['apples', 'oranges']); + var source = iD.BackgroundSource.template({ template: '{t}/{z}/{x}/{y}', subdomains: ['apples', 'oranges'] }); expect(source([0,1,1])).to.equal('oranges/1/0/1'); }); }); diff --git a/test/spec/renderer/hash.js b/test/spec/renderer/hash.js deleted file mode 100644 index b0e5b567f..000000000 --- a/test/spec/renderer/hash.js +++ /dev/null @@ -1,84 +0,0 @@ -describe("iD.Hash", function () { - var hash, map, controller; - - beforeEach(function () { - hash = iD.Hash(); - map = { - on: function () { return map; }, - zoom: function () { return arguments.length ? map : 0; }, - center: function () { return arguments.length ? map : [0, 0]; }, - centerZoom: function () { return arguments.length ? map : [0, 0]; } - }; - controller = { - on: function () { return controller; } - }; - }); - - afterEach(function () { - hash.map(null); - location.hash = ""; - }); - - describe("#map()", function () { - it("gets and sets map", function () { - expect(hash.controller(controller).map(map)).to.equal(hash); - expect(hash.map()).to.equal(map); - }); - - it("sets hadHash if location.hash is present", function () { - location.hash = "map=20.00/38.87952/-77.02405"; - hash.map(map); - expect(hash.hadHash).to.be.true; - }); - - it("centerZooms map to requested level", function () { - location.hash = "map=20.00/38.87952/-77.02405"; - sinon.spy(map, 'centerZoom'); - hash.map(map); - expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); - }); - - it("binds the map's move event", function () { - sinon.spy(map, 'on'); - hash.map(map); - expect(map.on).to.have.been.calledWith('move.hash', sinon.match.instanceOf(Function)); - }); - - it("unbinds the map's move event", function () { - sinon.spy(map, 'on'); - hash.map(map); - hash.map(null); - expect(map.on).to.have.been.calledWith('move.hash', null); - }); - }); - - describe("on window hashchange events", function () { - beforeEach(function () { - hash.map(map); - }); - - function onhashchange(fn) { - d3.select(window).one("hashchange", fn); - } - - it("centerZooms map at requested coordinates", function (done) { - onhashchange(function () { - expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); - done(); - }); - - sinon.spy(map, 'centerZoom'); - location.hash = "#map=20.00/38.87952/-77.02405"; - }); - }); - - describe("on map move events", function () { - it("stores the current zoom and coordinates in location.hash", function () { - sinon.stub(map, 'on') - .withArgs("move.hash", sinon.match.instanceOf(Function)) - .yields(); - hash.map(map); - expect(location.hash).to.equal("#map=0.00/0/0"); - }); - }); -}); diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js index d7d0e3440..de8abdf61 100644 --- a/test/spec/renderer/map.js +++ b/test/spec/renderer/map.js @@ -1,22 +1,10 @@ describe('iD.Map', function() { - var container, map; + var map; beforeEach(function() { - container = d3.select('body').append('div'); - map = iD.Map(); - container.call(map); - }); - - afterEach(function() { - container.remove(); - }); - - describe('#connection', function() { - it('gets and sets connection', function() { - var connection = iD.Connection(); - expect(map.connection(connection)).to.equal(map); - expect(map.connection()).to.equal(connection); - }); + map = iD().map(); + d3.select(document.createElement('div')) + .call(map); }); describe('#zoom', function() { diff --git a/test/spec/spec_helpers.js b/test/spec/spec_helpers.js index b8ebdf307..7a62e76b9 100644 --- a/test/spec/spec_helpers.js +++ b/test/spec/spec_helpers.js @@ -1,3 +1,12 @@ +iD.debug = true; + +mocha.setup({ + ui: 'bdd', + globals: ['__onresize.tail-size', '__onmousemove.zoom', '__onmouseup.zoom', '__onclick.draw'] +}); + +var expect = chai.expect; + chai.use(function (chai, utils) { var flag = utils.flag; diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 807a799f4..bbf8674ae 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -69,4 +69,46 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area:nth-child(1)')).to.be.classed('tag-landuse-park'); expect(surface.select('.area:nth-child(2)')).to.be.classed('tag-building-yes'); }); + + it("renders fills for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.select('.fill')).to.be.classed('relation'); + }); + + it("renders no strokes for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.selectAll('.stroke')[0].length).to.equal(0); + }); + + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([a, b, c, w, r]); + + surface.call(iD.svg.Areas(projection), graph, [w], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + expect(surface.select('.fill')).not.to.be.classed('tag-natural-wood'); + }); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 61c3229a9..989022632 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -39,6 +39,16 @@ describe("iD.svg.Lines", function () { expect(surface.select('.line')).to.be.classed('member-type-route'); }); + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var line = iD.Way(), + relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([line, relation]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + }); + it("preserves non-line paths", function () { var line = iD.Way(), graph = iD.Graph([line]); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js new file mode 100644 index 000000000..e00f6f78b --- /dev/null +++ b/test/spec/svg/midpoints.js @@ -0,0 +1,56 @@ +describe("iD.svg.Midpoints", function () { + var surface, + projection = Object, + filter = d3.functor(true); + + beforeEach(function () { + surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) + .call(iD.svg.Surface()); + }); + + it("finds the location of the midpoints", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [50, 0]}), + line = iD.Way({nodes: [a.id, b.id]}), + graph = iD.Graph([a, b, line]); + + // If no vertices are drawn, no midpoints are drawn. This dependence needs to be removed + surface.call(iD.svg.Vertices(projection), graph, [a], filter); + surface.call(iD.svg.Midpoints(projection), graph, [line], filter); + + expect(surface.select('.midpoint').datum().loc).to.eql([25, 0]); + }); + + it("doesn't create midpoints on segments with pixel length less than 40", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [39, 0]}), + line = iD.Way({nodes: [a.id, b.id]}), + graph = iD.Graph([a, b, line]); + + surface.call(iD.svg.Midpoints(projection), graph, [line], filter); + + expect(surface.selectAll('.midpoint')[0]).to.have.length(0); + }); + + it("binds a datum whose 'ways' property lists ways which include the segement", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [50, 0]}), + c = iD.Node({loc: [1, 1]}), + d = iD.Node({loc: [2, 2]}), + l1 = iD.Way({nodes: [a.id, b.id]}), + l2 = iD.Way({nodes: [b.id, a.id]}), + l3 = iD.Way({nodes: [c.id, a.id, b.id, d.id]}), + l4 = iD.Way({nodes: [a.id, d.id, b.id]}), + graph = iD.Graph([a, b, c, d, l1, l2, l3, l4]), + ab = function (d) { return d.id === [a.id, b.id].sort().join("-"); }; + + // If no vertices are drawn, no midpoints are drawn. This dependence needs to be removed + surface.call(iD.svg.Vertices(projection), graph, [a], filter); + surface.call(iD.svg.Midpoints(projection), graph, [l1, l2, l3, l4], filter); + + expect(surface.selectAll('.midpoint').filter(ab).datum().ways).to.eql([ + {id: l1.id, index: 1}, + {id: l2.id, index: 1}, + {id: l3.id, index: 2}]); + }); +}); diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js deleted file mode 100644 index 67f44ebae..000000000 --- a/test/spec/svg/multipolygons.js +++ /dev/null @@ -1,43 +0,0 @@ -describe("iD.svg.Multipolygons", function () { - var surface, - projection = Object, - filter = d3.functor(true); - - beforeEach(function () { - surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) - .call(iD.svg.Surface()); - }); - - it("adds relation and multipolygon classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('path')).to.be.classed('relation'); - expect(surface.select('path')).to.be.classed('multipolygon'); - }); - - it("adds tag classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon', boundary: "administrative"}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('.relation')).to.be.classed('tag-boundary'); - expect(surface.select('.relation')).to.be.classed('tag-boundary-administrative'); - }); - - it("preserves non-multipolygon paths", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.select('.layer-fill') - .append('path') - .attr('class', 'other'); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.selectAll('.other')[0].length).to.equal(1); - }); -}); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dd899e08e..048378815 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -19,6 +19,13 @@ describe("iD.svg.TagClasses", function () { expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); + it('adds tags based on the result of the `tags` accessor', function() { + selection + .datum(iD.Entity()) + .call(iD.svg.TagClasses().tags(d3.functor({highway: 'primary'}))); + expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); + }); + it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary') diff --git a/test/spec/translation.js b/test/spec/translation.js new file mode 100644 index 000000000..41f2e07df --- /dev/null +++ b/test/spec/translation.js @@ -0,0 +1,36 @@ +describe('translations', function() { + var languages = [], languageKeys = {}; + + function getKeys(lang, keys, prefix) { + keys = keys || []; + prefix = prefix || ''; + for (var i in lang) { + keys.push(prefix + i); + if (typeof lang[i] === 'object') { + getKeys(lang[i], keys, i + '.'); + } + } + return keys; + } + + describe('#translation-differences', function() { + + it('does not differ between languages', function() { + languages = _(locale).keys() + .without('current', '_current').value(); + + languageKeys = _.reduce(languages, function(mem, lang) { + mem[lang] = getKeys(locale[lang]); + return mem; + }, {}); + + var allkeys = _.flatten(_.values(languageKeys)); + + _.forEach(languageKeys, function(l, k) { + var diff = _.difference(allkeys, l).join(", "); + expect(diff).to.equal(""); + }); + }); + + }); +}); diff --git a/test/spec/ui/confirm.js b/test/spec/ui/confirm.js index a9380e1cc..59f1d06df 100644 --- a/test/spec/ui/confirm.js +++ b/test/spec/ui/confirm.js @@ -1,11 +1,19 @@ describe("iD.ui.confirm", function () { + + var elem; + beforeEach(function() { elem = d3.select('body').append('div'); }); + afterEach(function() { elem.remove(); }); + it('can be instantiated', function () { - var confirm = iD.ui.confirm(); + var confirm = iD.ui.confirm(elem); expect(confirm).to.be.ok; + happen.keydown(document, {keyCode: 27}); // dismiss }); + it('can be dismissed', function () { - var confirm = iD.ui.confirm(); + var confirm = iD.ui.confirm(elem); happen.click(confirm.select('button').node()); expect(confirm.node().parentNode).to.be.null; + happen.keydown(document, {keyCode: 27}); // dismiss }); }); diff --git a/test/spec/ui/flash.js b/test/spec/ui/flash.js index b0314ffeb..527e8f18b 100644 --- a/test/spec/ui/flash.js +++ b/test/spec/ui/flash.js @@ -1,6 +1,14 @@ describe("iD.ui.flash", function () { var clock; + var elem; + + beforeEach(function() { + elem = d3.select('body').append('div'); + }); + + afterEach(function() { elem.remove(); }); + beforeEach(function () { clock = sinon.useFakeTimers(); }); @@ -10,7 +18,7 @@ describe("iD.ui.flash", function () { }); it('leaves after 1000 ms', function () { - var flash = iD.ui.flash(); + var flash = iD.ui.flash(elem); clock.tick(1610); expect(flash.node().parentNode).to.be.null; }); diff --git a/test/spec/ui/geocoder.js b/test/spec/ui/geocoder.js index 54a7e571c..91e107ee2 100644 --- a/test/spec/ui/geocoder.js +++ b/test/spec/ui/geocoder.js @@ -1,6 +1,6 @@ -describe("iD.ui.geocoder", function () { +describe("iD.ui.Geocoder", function () { it('can be instantiated', function () { - var geocoder = iD.ui.geocoder(); + var geocoder = iD.ui.Geocoder(); expect(geocoder).to.be.ok; }); }); diff --git a/test/spec/ui/inspector.js b/test/spec/ui/inspector.js index eb5ea6be1..17160ddb4 100644 --- a/test/spec/ui/inspector.js +++ b/test/spec/ui/inspector.js @@ -1,10 +1,10 @@ -describe("iD.ui.inspector", function () { +describe("iD.ui.Inspector", function () { var inspector, element, tags = {highway: 'residential'}, - entity, graph; + entity, graph, context; function render() { - inspector = iD.ui.inspector().graph(graph); + inspector = iD.ui.Inspector().context(context); element = d3.select('body') .append('div') .attr('id', 'inspector-wrap') @@ -14,7 +14,7 @@ describe("iD.ui.inspector", function () { beforeEach(function () { entity = iD.Entity({type: 'node', id: "n12345", tags: tags}); - graph = iD.Graph([entity]); + context = iD(); render(); }); diff --git a/test/spec/ui/modal.js b/test/spec/ui/modal.js index 32a42878a..b91a05f99 100644 --- a/test/spec/ui/modal.js +++ b/test/spec/ui/modal.js @@ -1,8 +1,19 @@ describe("iD.ui.modal", function () { - it('can be instantiated', function () { - var modal = iD.ui.modal() + var elem; + + beforeEach(function() { + elem = d3.select('body').append('div'); + }); + + afterEach(function() { + elem.remove(); + }); + + it('can be instantiated', function() { + var modal = iD.ui.modal(elem) .select('.content') .text('foo'); expect(modal).to.be.ok; + happen.keydown(document, {keyCode: 27}); // dismiss }); }); diff --git a/test/spec/util.js b/test/spec/util.js index 9d5cf082b..d866a7b39 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -1,123 +1,24 @@ describe('iD.Util', function() { - var util; - - it('#trueObj', function() { + it('.trueObj', function() { expect(iD.util.trueObj(['a', 'b', 'c'])).to.eql({ a: true, b: true, c: true }); expect(iD.util.trueObj([])).to.eql({}); }); - it('#tagText', function() { + it('.tagText', function() { expect(iD.util.tagText({})).to.eql(''); - expect(iD.util.tagText({tags:{foo:'bar'}})).to.eql('foo: bar'); - expect(iD.util.tagText({tags:{foo:'bar',two:'three'}})).to.eql('foo: bar\ntwo: three'); + expect(iD.util.tagText({tags:{foo:'bar'}})).to.eql('foo=bar'); + expect(iD.util.tagText({tags:{foo:'bar',two:'three'}})).to.eql('foo=bar, two=three'); }); - it('#stringQs', function() { + it('.stringQs', function() { expect(iD.util.stringQs('foo=bar')).to.eql({foo: 'bar'}); expect(iD.util.stringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); expect(iD.util.stringQs('')).to.eql({}); }); - it('#qsString', function() { + it('.qsString', function() { expect(iD.util.qsString({ foo: 'bar' })).to.eql('foo=bar'); expect(iD.util.qsString({ foo: 'bar', one: 2 })).to.eql('foo=bar&one=2'); expect(iD.util.qsString({})).to.eql(''); }); - - describe('geo', function() { - describe('#roundCoords', function() { - expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]); - }); - - describe('#interp', function() { - it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]); - }); - it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]); - }); - }); - - describe('#dist', function() { - it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; - expect(iD.geo.dist(a, b)).to.eql(0); - }); - it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; - expect(iD.geo.dist(a, b)).to.eql(10); - }); - it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geo.dist(a, b)).to.eql(5); - }); - }); - - describe('#pointInPolygon', function() { - it('says a point in a polygon is on a polygon', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 0.5]; - expect(iD.geo.pointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [ - [0, 0], - [0, 1], - [1, 1], - [1, 0], - [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geo.pointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('#polygonContainsPolygon', function() { - it('says a polygon in a polygon is in', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.true; - }); - it('says a polygon outside of a polygon is out', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('#polygonIntersectsPolygon', function() { - it('says a polygon in a polygon intersects it', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('says a polygon that partially intersects does', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; - expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; - }); - - it('says totally disjoint polygons do not intersect', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; - expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('#pathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geo.pathLength(path)).to.eql(6); - }); - }); - }); });