Tagging work in progress

This commit is contained in:
Richard Fairhurst
2012-10-12 00:32:13 +01:00
parent 3744ca602d
commit 50f7a5dcfc
44 changed files with 334 additions and 20 deletions

View File

@@ -48,6 +48,6 @@ Scoping in JavaScript is famously broken: Dojo's lang.hitch method will save you
Instance methods and variables _always_ need to be accessed with 'this.'. This is a fairly frequent gotcha if you're coming from another language.
The [Dojo site](http://dojotoolkit.org/) has lots of documentation. iD currently uses Dojo 1.7.2 - this has a much better module architecture (AMD) than previously.
The [Dojo site](http://dojotoolkit.org/) has lots of documentation. iD currently uses Dojo 1.8 - this has a much better module architecture (AMD) than previously.
Come on in, the water's lovely. More help? Ping RichardF on IRC (irc.oftc.net, in #osm-dev or #osm), on the OSM mailing lists or at richard@systemeD.net.

View File

@@ -18,6 +18,9 @@ Renderer to do
* Node headings (for locks etc.)
* Dragging needs tolerance (i.e. less than n pixels and n seconds)
Tagging to do
* dijitTooltipConnector is wrong when an entityUI is clicked
ControllerStates to do:
* Try to share as much code as possible
* Draw way controller states:

View File

@@ -4,12 +4,12 @@
<meta charset="utf-8">
<title>iD</title>
<!-- load Dojo -->
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.7.2/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.7.2/dojox/layout/resources/FloatingPane.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.7.2/dojox/layout/resources/ResizeHandle.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8/dijit/themes/claro/claro.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8/dojox/layout/resources/FloatingPane.css">
<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/dojo/1.8/dojox/layout/resources/ResizeHandle.css">
<link rel="stylesheet" href="css/app.css">
<script src="js/lib/jshashtable.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.7.2/dojo/dojo.js" data-dojo-config="async: true, parseOnLoad: true, baseUrl: 'js/iD/'"></script>
<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.8/dojo/dojo.js" data-dojo-config="async: true, parseOnLoad: true, baseUrl: 'js/iD/'"></script>
<style type="text/css">
:focus { outline-color: transparent; outline-style: none; }
* { font-family: Helvetica, Arial; }
@@ -22,7 +22,7 @@
<script>
require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom",
require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom","dojo/Evented",
"dijit/form/Button","dijit/form/ToggleButton",
"dojox/layout/FloatingPane",
"iD/actions/UndoStack","iD/actions/CreatePOIAction",
@@ -31,7 +31,7 @@ require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/
"iD/controller/edit/NoSelection","iD/controller/shape/NoSelection",
"iD/renderer/Map","iD/styleparser/RuleSet",
"iD/ui/DragAndDrop","iD/ui/StepPane",
"dojo/domReady!"], function(lang,domGeom,domClass,on,dom){
"dojo/domReady!"], function(lang,domGeom,domClass,on,dom,Evented){
var ruleset=new iD.styleparser.RuleSet();
var conn=new iD.Connection("http://www.overpass-api.de/api/xapi?");
@@ -58,8 +58,8 @@ require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/
map.setController(controller);
// Initialise event listeners
on(window, "enterState", enterStateListener);
on(window, "exitState", exitStateListener);
controller.on("enterState", enterStateListener);
controller.on("exitState", exitStateListener);
// ----------------------------------------------------
// Data is loaded and app ready to go
@@ -73,6 +73,10 @@ require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/
// Set initial controllerState
controller.setState(new iD.controller.edit.NoSelection());
// Load presets
controller.setTagPresets('way','presets/ways.json');
controller.setTagPresets('node','presets/nodes.json');
// Load data
map.download();
@@ -122,7 +126,7 @@ require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/
<div id="modebuttons" style="position:absolute; left: 10px; top: 10px;">
<div id="addPOI" data-dojo-type="dijit.form.DropDownButton" data-dojo-props="iconClass:'dijitIconApplication', onClick:function(){}">
<span>Add point</span>
        <div data-dojo-type="dijit.TooltipDialog">
<div data-dojo-type="dijit.TooltipDialog">
<p>Drag points onto the map</p>
<table id="dndgrid">
</table>

View File

@@ -1,38 +1,48 @@
// iD/Controller.js
define(['dojo/_base/declare','dojo/on','iD/actions/UndoStack'], function(declare,on){
define(['dojo/_base/declare','dojo/on','dojo/Evented',
'iD/actions/UndoStack','iD/tags/PresetList'], function(declare,on,Evented){
// ----------------------------------------------------------------------
// Controller base class
declare("iD.Controller", null, {
declare("iD.Controller", [Evented], {
state: null, // current ControllerState
map: null, // current Map
stepper: null, // current StepPane
undoStack: null, // main undoStack
presets: null, // tag presets
editorCache: null, // cache of tag editor objects, means we don't have to repeatedly load them
constructor:function(_map) {
// summary: The Controller marshalls ControllerStates and passes events to them.
this.map=_map;
this.undoStack=new iD.actions.UndoStack();
this.presets={};
this.editorCache={};
},
setStepper:function(_stepper) {
// summary: Set reference for the singleton-like class for the step-by-step instruction panel.
this.stepper=_stepper;
},
setTagPresets:function(type,url) {
// summary: Load and store a JSON tag preset file.
this.presets[type]=new iD.tags.PresetList(type,url);
},
setState:function(newState) {
// summary: Enter a new ControllerState, firing exitState on the old one, and enterState on the new one.
if (newState==this.state) { return; }
if (this.state) {
this.state.exitState(newState);
on.emit(window, "exitState", { bubbles: true, cancelable: true, state: this.state.stateNameAsArray() });
this.emit("exitState", { bubbles: true, cancelable: true, state: this.state.stateNameAsArray() });
}
newState.setController(this);
this.state=newState;
newState.enterState();
on.emit(window, "enterState", { bubbles: true, cancelable: true, state: this.state.stateNameAsArray() });
this.emit("enterState", { bubbles: true, cancelable: true, state: this.state.stateNameAsArray() });
},
entityMouseEvent:function(event,entityUI) {

View File

@@ -11,7 +11,7 @@ define(['dojo/_base/declare','dojo/_base/array','dojo/_base/lang',
declare("iD.Entity", null, {
connection: null,
id: NaN,
id: NaN,
loaded: false,
tags: null,
entityType: '',
@@ -106,6 +106,13 @@ declare("iD.Entity", null, {
}
return n.length==0 ? 'unknown' : n.join('; '); // String
},
matchesTags:function(hash) {
for (var k in hash) {
if (!this.tags[k] || this.tags[k]!=hash[k]) return false;
}
return true;
},
// ---------------
// Parent-handling

View File

@@ -1,7 +1,8 @@
// iD/controller/edit/EditBaseState.js
define(['dojo/_base/declare','dijit/TooltipDialog','dijit/popup',
'iD/controller/ControllerState'], function(declare){
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/array','dojo/on',
'dijit/registry','dijit/TooltipDialog','dijit/Dialog','dijit/popup',
'iD/controller/ControllerState','iD/tags/TagEditor'], function(declare,lang,array,on,registry){
// ----------------------------------------------------------------------
// EditBaseState class - provides shared UI functions to edit mode states
@@ -21,18 +22,30 @@ declare("iD.controller.edit.EditBaseState", [iD.controller.ControllerState], {
// entity: iD.Entity The entity to be edited.
var h=entity.friendlyName(); h = (h=='') ? h : h+"<br/>";
this.editortooltip = new dijit.TooltipDialog({
content: h+"<button data-dojo-type='dijit.form.Button' type='submit'>Edit tags</button> "
+"<button data-dojo-type='dijit.form.Button' type='submit'>Edit shape</button> ",
content: h+"<button data-dojo-type='dijit.form.Button' id='editTags' parseOnLoad='false' type='submit'>Edit tags</button> "
+"<button data-dojo-type='dijit.form.Button' id='editShape' parseOnLoad='false' type='submit'>Edit shape</button> ",
autoFocus: false
});
on(registry.byId('editTags'), 'click', lang.hitch(this,this.editTags,entity));
dijit.popup.open({ popup: this.editortooltip, x: x, y: y });
},
closeEditorTooltip:function() {
// summary: Close the tooltip.
array.forEach(['editTags','editShape'], function(b){
if (!registry.byId(b)) return;
registry.byId(b).type=''; // fix Safari issue
registry.byId(b).destroy(); // remove from registry so we can reinitialise next time
});
if (this.editortooltip) { dijit.popup.close(this.editortooltip); }
},
editTags:function(entity) {
// summary: Open a tag editor for the selected entity.
var tagEditor = new iD.tags.TagEditor(entity,this.controller);
this.closeEditorTooltip();
},
});
// ----------------------------------------------------------------------

View File

@@ -143,7 +143,7 @@ declare("iD.styleparser.ShapeStyle", [iD.styleparser.Style], {
};
},
shapeStrokeStyler:function() {
if (isNaN(this.casing_color)) { return { width:0 }; }
if (isNaN(this.casing_color)) { return { width:0 }; }
return {
color: this.dojoColor(this.casing_color, this.casing_opacity ? this.casing_opacity : 1),
width: this.casing_width ? this.casing_width : 1

50
js/iD/tags/PresetList.js Normal file
View File

@@ -0,0 +1,50 @@
// iD/tags/PresetList.js
// List of presets for a given type (e.g. nodes, ways)
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/xhr'], function(declare,lang,xhr){
declare("iD.tags.PresetList", null, {
entityType: null,
presets: null,
constructor:function(_type,_url) {
// summary: List of presets for a given type (e.g. nodes, ways)
this.entityType=_type;
dojo.xhrGet({
url: _url,
handleAs: "json",
load: lang.hitch(this, this.loaded),
error: function(err) { console.log("Couldn't load presets"); }
});
},
loaded:function(_obj) {
this.presets=_obj;
console.log("Loaded presets for "+this.entityType);
},
assembleEditorsForEntity:function(_entity) {
if (_entity.entityType!=this.entityType) return false;
var editorList=[];
for (var group in this.presets) {
for (var preset in this.presets[group]) {
var props=this.presets[group][preset];
if (_entity.matchesTags(props.tags)) {
for (var i in props.editors) {
var editor=props.editors[i];
if (editorList.indexOf(editor)==-1) { editorList.push(editor); }
}
}
}
}
return editorList;
}
});
// ----------------------------------------------------------------------
// End of module
});

100
js/iD/tags/TagEditor.js Normal file
View File

@@ -0,0 +1,100 @@
// iD/tags/TagEditor.js
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/xhr','dojo/dom-construct',
'dijit/Dialog','dijit/form/Form','dijit/form/Button','dijit/form/TextBox'],
function(declare,lang,xhr,domConstruct){
declare("iD.tags.TagEditor", null, {
entity: null,
controller: null,
dialog: null,
editorContainers: null, // hash of DOM nodes to put editors in
constructor:function(_entity,_controller) {
// summary: Construct a tag editor dialog box.
console.log("TagEditor constructor");
this.entity=_entity;
this.controller=_controller;
this.editorContainers={};
// Create the dialog, and the form to put the editors in
this.dialog = new dijit.Dialog({ title: "My Dialog", content: "Test content.", style: "width: 300px" });
var form = new dijit.form.Form({ encType: 'multipart/form-data', action: '', method: '',
onSubmit: function(event) { console.log('submit'); }
}, dojo.doc.createElement('div'));
this.dialog.set('content',form);
this.dialog.show();
// Add each editor
var presetList=this.controller.presets[_entity.entityType];
var editors=presetList.assembleEditorsForEntity(_entity);
for (var i in editors) {
var editor=editors[i];
this.appendEditor(editor,form.domNode);
}
},
appendEditor:function(_editor,_destination) {
// summary: Request an editor (cached if available, XHR if not), and call renderEditor when it's available.
if (this.controller.editorCache[_editor]) {
this.renderEditor(_editor,_destination);
} else {
dojo.xhrGet({
url: "presets/editors/"+_editor+".json",
handleAs: "json",
load: lang.hitch(this, this.loadedEditor, _editor, _destination),
error: function(err) { console.log("Couldn't load editor"); }
});
}
},
loadedEditor:function(_editor,_destination,_obj) {
// summary: Editor has loaded via XHR, so store it in the cache and render it.
this.controller.editorCache[_editor]=_obj;
this.renderEditor(_editor,_destination);
},
renderEditor:function(_editor,_destination) {
// summary: Render an editor as a form.
var editor=this.controller.editorCache[_editor];
// Add the subhead
var element=domConstruct.create('h3');
element.appendChild(dojo.doc.createTextNode(_editor));
_destination.appendChild(element);
// Add each form element
for (var label in editor) {
var item=editor[label];
var value=this.getTagValue(item.key);
element=domConstruct.create('div');
switch (item.type) {
case 'text':
var textbox = new dijit.form.TextBox({ name: item.key, value: value, type: 'text' }, domConstruct.create('input'));
element.appendChild(dojo.doc.createTextNode(label));
element.appendChild(textbox.domNode);
break;
case 'dropdown':
case 'relation':
case 'hidden':
}
_destination.appendChild(element);
}
// var submitbtn = new dijit.form.Button({ name: 'submit', type: 'submit', value: 'Submit', label: "Submit" }, dojo.doc.createElement('button'));
// var resetbtn = new dijit.form.Button({ type: 'reset', label: 'Reset' }, dojo.doc.createElement('button'));
// _destination.appendChild(submitbtn.domNode);
// _destination.appendChild(resetbtn.domNode);
},
getTagValue:function(k) {
// summary: Get the value of a tag for the current entity, or empty string if unset.
return this.entity.tags[k] ? this.entity.tags[k] : '';
},
});
// ----------------------------------------------------------------------
// End of module
});

2
presets/editors/bus.json Normal file
View File

@@ -0,0 +1,2 @@
{
}

View File

@@ -0,0 +1,20 @@
{
'1': { key: 'type', value: 'route', type: 'hidden' },
'2': { key: 'route', value: 'bicycle', type: 'hidden' },
'Route name': {
key: 'name',
type: 'text'
},
'Route number': {
key: 'ref',
type: 'text'
},
'Network': {
key: 'network',
type: 'dropdown',
value: { 'National': 'ncn', 'Regional': 'rcn', 'Local': 'lcn' }
}
}

View File

@@ -0,0 +1,12 @@
{
'Bikes allowed?': {
key: 'bicycle',
type: 'dropdown',
value: { 'Yes': 'yes', 'No': 'no', 'Permissive': 'permissive' }
},
'Cycle routes': {
type: 'relation',
editor: 'cycle_route'
}
}

View File

@@ -0,0 +1,2 @@
{
}

11
presets/editors/road.json Normal file
View File

@@ -0,0 +1,11 @@
{
'Road name': {
key: 'name',
type: 'text'
},
'Road number': {
key: 'ref',
type: 'text'
}
}

View File

@@ -0,0 +1,25 @@
'Mapzen' lets you create and maintain OpenStreetMap maps.
Copyright (C) 2012 Cloud Made Ltd (CloudMade) mapzen@cloudmade.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
presets/nodes.json Normal file
View File

@@ -0,0 +1,2 @@
{
}

53
presets/ways.json Normal file
View File

@@ -0,0 +1,53 @@
{
'Roads': {
'Motorway': {
tags: { highway: 'motorway' },
implied: { oneway: 'yes', bicycle: 'no', foot: 'no', horse: 'no' },
icon: 'ways/highway__motorway.png',
editors: ['road']
},
'Trunk road': {
tags: { highway: 'secondary' },
icon: 'ways/highway__secondary.png',
editors: ['road','cycling','bus']
},
'Primary road': {
tags: { highway: 'primary' },
icon: 'ways/highway__primary.png',
editors: ['road','cycling','bus']
},
'Secondary road': {
tags: { highway: 'secondary' },
icon: 'ways/highway__secondary.png',
editors: ['road','cycling','bus']
},
'Tertiary road': {
tags: { highway: 'tertiary' },
icon: 'ways/highway__tertiary.png',
editors: ['road','cycling','bus']
},
'Minor road': {
tags: { highway: 'unclassified' },
icon: 'ways/highway__unclassified.png',
editors: ['road','cycling','bus']
},
},
'Railways': {
'Railway': {
tags: { railway: 'rail' },
icon: 'ways/railway__rail.png',
editors: ['rail']
},
'Disused tracks': {
tags: { railway: 'disused' },
icon: 'ways/railway__disused.png',
editors: ['rail']
},
'Abandoned trackbed': {
tags: { railway: 'abandoned' },
icon: 'ways/railway__abandoned.png',
editors: ['rail']
},
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB