Current working version

This commit is contained in:
Richard Fairhurst
2012-07-11 14:27:30 +01:00
parent 790d71bba4
commit 22fab3eb1d
48 changed files with 4088 additions and 2 deletions

View File

@@ -1,2 +1,51 @@
iD
==
iD - JavaScript beginners' editor for OpenStreetMap
===================================================
Basics
------
* iD is a JavaScript-based OpenStreetMap editor with MapCSS rendering.
* iD is written with the Dojo framework.
* It's intentionally simple. iD is not a 90% editor. It's not even a 70% editor. It should let you do the most basic tasks while not breaking other people's data. Nothing more.
* Same goes for the code. So go easy on the abstraction. :)
* Speaking of percentages, it's about 1% complete.
* We're initially targeting WebKit-based browsers and Firefox, using SVG. IE and non-SVG can come later!
* The licence of iD is WTFPL and contributions to 'trunk' should accord with this. This does of course allow you to dual-license.
Getting started
---------------
* Unzip and start playing!
* All the code is in js/iD.
How it works
------------
The code works similarly to Potlatch 2, but with a bit less abstraction. So, we have:
* Connection: stores, fetches and saves data. (iD/Connection.js)
* Entity (Node, Way, Relation): the data objects. (iD/Entity.js)
* EntityUI (NodeUI, WayUI): the rendered graphic elements. (iD/renderer/...)
* Map: the displayed map on which EntityUIs are rendered. (iD/renderer/Map.js)
* Controller: the heart of the app, which does its work via...
* ControllerState: the current UI mode. ControllerStates decide what to do in response to mouse/keyboard events. (iD/controller/...)
* UndoableAction: the code to actually change the data, as fired by ControllerStates. (iD/actions/...)
The UI is more modal than Potlatch 2. In particular there's a "draw shape" mode (the "Add road or shape" button) and an "edit object" mode. The directory structure of iD/controller reflects this.
Other relevant code includes the MapCSS parser in styleparser/ and custom widgets in ui/ .
Getting started
---------------
Most of the interesting code is in the ControllerStates, which live in iD/controller/. Each one corresponds to a UI mode (e.g. "drawing a way"). Its EntityMouseEvent method takes the user's mouse event (e.g. "clicked on a node"), carries out any actions, and returns the new ControllerState (which might just be 'this', i.e. carry on with the current state).
Coding tips
-----------
Scoping in JavaScript is famously broken: Dojo's lang.hitch method will save your life. Make sure you include dojo/_base/lang (in the 'declare' statement). Then, when you're passing an instance method as a function parameter, use lang.hitch(instance, instance.method) instead, and Dojo will magically set the right scope. You'll see lots of examples of this throughout the code.
Instance methods and variables _always_ need to be accessed with 'this.'. This is a fairly frequent gotcha if you're coming from another language.
You'll find various notes and comments in the docs/ folder. Feel free to add to these.
More help: ping RichardF on IRC (irc.oftc.net, in #osm-dev or #osm), on the OSM mailing lists or at richard@systemeD.net.

3
css/app.css Normal file
View File

@@ -0,0 +1,3 @@
/* Additional CSS rules will go here */
.currentMode { font-weight: bold; }

45
docs/coding_standards.txt Executable file
View File

@@ -0,0 +1,45 @@
Coding standards and advice for iD
==================================
Classes
-------
All constructors must initialise objects and arrays. It is not enough to say
array:[],
constructor:function() {
},
but rather, you should do
array: null, // effectively a placeholder
constructor:function() {
this.array=[],
},
or bad things will happen. You should still declare the object outside the constructor, but for clarity rather than functionality.
(This doesn't apply to simple types - numbers, strings, booleans - which you can declare as normal.)
Function names
--------------
Anything that creates and calls an Action should be prefixed with do:
doSetLatLon(lat,lon)
Anything that is called by an Action, to do the actual work, should be prefixed with an underscore:
_setLatLon(lat,lon)
and commented as such.
File naming
-----------
The filename should be the name of the base class. You can add subclasses within that file for clarity. Don't add extra classes that aren't subclasses, unless they're not referenced from elsewhere.
Class and variable names
------------------------
You can prefix function arguments with an underscore to make it clear where they've come from.
Layout
------
* Hard tabs, indent of 4.
* Do not indent the root level of the module. Add an 'End of module' comment instead.
Useful stuff to know about Dojo
-------------------------------
* The array and lang modules are full of useful add-ons to basic JavaScript functionality. lang/hitch will save your life with scopes.

7
docs/controllerstates.txt Executable file
View File

@@ -0,0 +1,7 @@
Each ControllerState represents a UI state.
They are grouped into folders:
- 'edit' is all states within the 'Edit object' mode
- 'shape' is all states within the 'Add shape' mode
- 'point' is all states within the 'Add point' mode

14
docs/events.txt Executable file
View File

@@ -0,0 +1,14 @@
== Entities ==
Listeners are created in NodeUI and WayUI for each item's hitzone.
MouseEvents proceed like this:
* Event triggered on EntityUI, calling EntityUI.entityMouseEvent(event)
* This calls Controller.entityMouseEvent(event,entityUI)
* This updates the state from ControllerState.processMouseEvent(event,entityUI)
== Non-entities ==
* onmousemove in Map.js calls processMove, which calls Controller.entityMouseEvent(event,null)
* onclick in Map.js calls clickSurface, which calls Controller.entityMouseEvent(event,null)

63
docs/todo.txt Executable file
View File

@@ -0,0 +1,63 @@
DrawWay to do:
* make junctions
* still allow dragging the map
Drag and drop to do:
* icon grid
* improve 'avatar'
* cope with dragging onto anything, not just blank areas of the map
Next to do:
* drag nodes
* remove trailing commas (for IE!)
Renderer to do
* hover!
* NodeUI renderer needs a default width/height so that clicking a POI without them still shows the highlight
* Fix 'special values' in RuleSet - they're not really special for SVG etc.
* Labels other than text-on-path
* Node headings (for locks etc.)
* Dragging needs tolerance (i.e. less than n pixels and n seconds)
ControllerStates to do:
* Try to share as much code as possible
* Draw way controller states:
- NoSelection (next click starts, or selects)
- SelectedWay (next click starts, or selects)
- SelectedPOINode (next click starts, or selects)
- DrawWay (next click continues, or completes and edits)
Events
* EntityMouseEvent seems to get called twice most of the time when moving the mouse around in DrawWay
* Maybe we should just broadcast to the Controller, and the Controller knows what to do. In other words:
- undo says "the following entities have changed: way 5, node 3, way 7, relation 8"
- the Controller gets the message and invokes a redraw
- no need for anything else to listen
General code
* Do the ***doSetLatLon*** and ***_setLatLon*** naming convention
------------------------------------------------
Modes:
1. Add point
2. Add street or shape
3. Edit object
3a. edit tags (and relation memberships, etc.)
3b. move object
3c. delete object
3d. extend way
3e. geometry operations (like P2 Toolbox)
In 'edit': double-clicking goes into "draw shape"; double-clicking then Enter creates POI
Needs greyed-out "step-by-step" window for draw modes:
Click at the start point to begin drawing
Add each point, click by click
Double-click when you've finished
Then choose what it is
With buttons at the bottom:
Finish
Cancel
Undo last

11
draganddrop.json Normal file
View File

@@ -0,0 +1,11 @@
[
{
name: "Cafe",
icon: "cafe.png",
tags: "amenity=cafe"
}, {
name: "Pub",
icon: "pub.png",
tags: "amenity=pub"
}
]

BIN
icons/cafe.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

BIN
icons/fast_food.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

BIN
icons/parking_cycle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

BIN
icons/pub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

BIN
icons/restaurant.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

BIN
icons/school.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

171
index.html Executable file
View File

@@ -0,0 +1,171 @@
<!DOCTYPE html>
<html>
<head>
<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="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>
<style type="text/css">
:focus { outline-color: transparent; outline-style: none; }
* { font-family: Helvetica, Arial; }
</style>
</head>
<body class="claro">
<div id="appLayout" class="demoLayout">
<script>
require(["dojo/_base/lang","dojo/dom-geometry","dojo/dom-class","dojo/on",
"dijit/form/Button","dijit/form/ToggleButton",
"dojox/layout/FloatingPane",
"iD/actions/UndoStack","iD/actions/CreatePOIAction",
"iD/Connection",
"iD/Controller",
"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){
var ruleset=new iD.styleparser.RuleSet();
var conn=new iD.Connection("http://www.overpass-api.de/api/xapi?");
// Load styles
ruleset.registerCallback(styleLoaded);
ruleset.loadFromCSS("potlatch.css",styleLoaded);
// Initialise map
var map = new iD.renderer.Map(51.87,-1.49,17,"map",conn);
conn.registerMap(map);
map.ruleset=ruleset;
// Initialise controller
var controller=new iD.Controller(map);
map.setController(controller);
// Initialise event listeners
on(window, "enterState", enterStateListener);
on(window, "exitState", exitStateListener);
// ----------------------------------------------------
// Data is loaded and app ready to go
function styleLoaded() {
console.log("style loaded");
// Initialise drag-and-drop icons
new iD.ui.DragAndDrop("map",map,"dndgrid");
// Initialise help pane
controller.setStepper(new iD.ui.StepPane("helpPane","helpSteps"));
// Set initial controllerState
controller.setState(new iD.controller.edit.NoSelection());
// Load data
map.download();
}
// ----------------------------------------------------
// State event listeners
function enterStateListener(event) {
domClass.add(event.state[0]+"Button","currentMode");
};
function exitStateListener(event) {
domClass.remove(event.state[0]+"Button","currentMode");
};
// ----------------------------------------------------
// Mode button handlers
enterWayMode=function() {
console.log('Second button was clicked!');
controller.setState(new iD.controller.shape.NoSelection());
};
enterEditMode=function() {
console.log('Third button was clicked!');
};
finishClicked=function() {
controller.stepper.hide();
};
cancelClicked=function() {
controller.stepper.hide();
};
// ----------------------------------------------------
// Map control handlers
zoomInClicked =function() { map.zoomIn(); };
zoomOutClicked=function() { map.zoomOut(); };
});
</script>
<!-- see http://dojotoolkit.org/documentation/tutorials/1.7/themes_buttons_textboxes/
and http://dojotoolkit.org/widgets -->
<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(){ console.log('First button was clicked!'); }">
<span>Add point</span>
        <div data-dojo-type="dijit.TooltipDialog">
<p>Drag points onto the map</p>
<table id="dndgrid">
</table>
</div>
</div><script>require(["dijit/form/DropDownButton", "dijit/TooltipDialog","dojo/dnd/Source","dojo/parser", "dojo/domReady!"]);</script>
<button id="shapeButton" data-dojo-type="dijit.form.ToggleButton" data-dojo-props="onClick:enterWayMode">
Add road or shape
</button><script>require(["dijit/form/ToggleButton", "dojo/parser", "dojo/domReady!"]);</script>
<button id="editButton" data-dojo-type="dijit.form.Button" data-dojo-props="onClick:enterEditMode">
Edit object
</button><script>require(["dijit/form/Button", "dojo/parser", "dojo/domReady!"]);</script>
</div>
<div id="zoombuttons">
<button style="position: absolute; left: 10px; top: 40px;"
id="zoomIn" data-dojo-type="dijit.form.Button" data-dojo-props="onClick:zoomInClicked">
+
</button>
<button style="position: absolute; left: 10px; top: 70px;"
id="zoomOut" data-dojo-type="dijit.form.Button" data-dojo-props="onClick:zoomOutClicked">
&ndash;
</button>
</div>
<!-- Map div -->
<div id="map"
style="-webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;">
</div>
<!-- Floating help window -->
<div id="helpPane" data-dojo-type="dojox.layout.FloatingPane"
data-dojo-props="resizable:true, closable:false, dockable:false, title: 'Step by step'"
style="position:absolute; top:50px; left:10px; width:200px; height:200px; visibility: hidden;" >
<ol id="helpSteps">
</ol>
<button id="finishHelpButton" data-dojo-type="dijit.form.Button" data-dojo-props="onClick:finishClicked">
Finish
</button><script>require(["dijit/form/Button", "dojo/parser", "dojo/domReady!"]);</script>
<button id="cancelHelpButton" data-dojo-type="dijit.form.Button" data-dojo-props="onClick:cancelClicked" style="float:right">
Cancel
</button><script>require(["dijit/form/Button", "dojo/parser", "dojo/domReady!"]);</script>
</div>
</div><!-- applayout -->
</body>
</html>

249
js/iD/Connection.js Executable file
View File

@@ -0,0 +1,249 @@
// iD/Connection.js
define(["dojo/_base/xhr","dojo/_base/lang","dojox/xml/DomParser","dojo/_base/array",'dojo/_base/declare',
"iD/Entity","iD/actions/CreateEntityAction"], function(xhr,lang,DomParser,array,declare,Entity){
// ----------------------------------------------------------------------
// Connection base class
declare("iD.Connection", null, {
nodes: {}, // hash of node objects
ways: {}, // hash of way objects
relations: {}, // hash of relation objects
pois: null, // list of nodes which are POIs
maps: [], // list of Map objects listening to this
callback: null, // callback once .osm is parsed
modified: false, // data has been changed
nextNode: -1, // next negative ids
nextWay: -1, // |
nextRelation: -1, // |
apiBaseURL: '', // root API address
constructor:function(_apiURL) {
console.log("Created a connection");
this.nodes={};
this.ways={};
this.relations={};
this.pois=new Hashtable();
this.maps=[];
this.modified=false;
this.apiBaseURL=_apiURL;
},
assign:function(obj) {
switch (obj.entityType) {
case "node": this.nodes[obj.id]=obj; break;
case "way": this.ways[obj.id]=obj; break;
case "relation": this.relations[obj.id]=obj; break;
}
},
getNode:function(id) { return this.nodes[id]; },
getWay:function(id) { return this.ways[id]; },
getRelation:function(id) { return this.relations[id]; },
getOrCreate:function(id,type) {
switch (type) {
case "node":
if (!this.nodes[id]) this.assign(new iD.Node(this, id, NaN, NaN, {}, false));
return this.nodes[id];
case "way":
if (!this.ways[id]) this.assign(new iD.Way(this, id, [], {}, false));
return this.ways[id];
case "relation":
if (!this.relations[id]) this.assign(new iD.Relation(this, id, [], {}, false));
return this.relations[id];
}
},
createNode:function(tags, lat, lon, perform) {
var node = new iD.Node(this, this.nextNode--, lat, lon, tags, true);
perform(new iD.actions.CreateEntityAction(node, lang.hitch(this,this.assign) ));
return node;
},
createWay:function(tags, nodes, perform) {
var way = new iD.Way(this, this.nextWay--, nodes.concat(), tags, true);
perform(new iD.actions.CreateEntityAction(way, lang.hitch(this,this.assign) ));
return way;
},
createRelation:function(tags, members, perform) {
var relation = new iD.Relation(this, this.nextRelation--, members.concat(), tags, true);
perform(new iD.actions.CreateEntityAction(relation, lang.hitch(this,this.assign) ));
return relation;
},
markClean:function() { this.modified=false; },
markDirty:function() { this.modified=true; },
isDirty:function() { return this.modified; },
getObjectsByBbox:function(left,right,top,bottom) {
var o={ poisInside: [], poisOutside: [],
waysInside: [], waysOutside: [] };
for (var id in this.ways) {
var way=this.ways[id];
if (way.within(left,right,top,bottom)) { o.waysInside.push(way); }
else { o.waysOutside.push(way); }
}
this.pois.each(function(node,v) {
if (node.within(left,right,top,bottom)) { o.poisInside.push(node); }
else { o.poisOutside.push(node); }
});
return o;
},
// Redraw handling
registerMap:function(map) {
this.maps.push(map);
},
refreshMaps:function() {
array.forEach(this.maps, function(map) {
map.updateUIs(false,true);
});
},
refreshEntity:function(_entity) {
array.forEach(this.maps, function(map) {
map.refreshUI(_entity);
});
},
// Callback when completed loading (used in initialisation)
registerCallback:function(_callback) {
this.callback=_callback;
},
// POI handling
updatePOIs:function(nodelist) {
for (var i in nodelist) {
if (nodelist[i].hasParentWays()) {
this.pois.remove(nodelist[i]);
} else {
this.pois.put(nodelist[i],true);
}
}
},
getPOIs:function() {
return this.pois.keys();
},
registerPOI:function(node) {
this.pois.put(node,true);
},
unregisterPOI:function(node) {
this.pois.remove(node);
},
// OSM parser
loadFromAPI:function(left,right,top,bottom) {
var url="http://www.overpass-api.de/api/xapi?map?bbox="+left+","+bottom+","+right+","+top;
xhr.get({ url: url,
headers: { "X-Requested-With": null },
load: lang.hitch(this, "processOSM") });
},
loadFromURL:function(url) {
xhr.get({ url: url, load: lang.hitch(this, "processOSM") });
},
processOSM:function(result) {
var jsdom = DomParser.parse(result).childNodes[1];
var nodelist = [];
for (var i in jsdom.childNodes) {
var obj=jsdom.childNodes[i];
switch(obj.nodeName) {
case "node":
var node = new iD.Node(this,
getAttribute(obj,'id'),
getAttribute(obj,'lat'),
getAttribute(obj,'lon'),
getTags(obj));
this.assign(node);
nodelist.push(node);
break;
case "way":
var way = new iD.Way(this,
getAttribute(obj,'id'),
getNodes(obj,this),
getTags(obj));
this.assign(way);
break;
case "relation":
var relation = new iD.Relation(this,
getAttribute(obj,'id'),
getMembers(obj,this),
getTags(obj));
this.assign(relation);
break;
}
}
this.updatePOIs(nodelist);
this.refreshMaps();
if (this.callback) { this.callback(); }
// Private functions to parse DOM created from XML file
function getAttribute(obj,name) {
var result=array.filter(obj.attributes,function(item) {
return item.nodeName==name;
});
return result[0].nodeValue;
}
function getTags(obj) {
var tags={};
array.forEach(obj.childNodes,function(item) {
if (item.nodeName=='tag') {
tags[getAttribute(item,'k')]=getAttribute(item,'v');
}
});
return tags;
}
function getNodes(obj,conn) {
var nodes=[];
array.forEach(obj.childNodes,function(item) {
if (item.nodeName=='nd') {
var id=getAttribute(item,'ref');
nodes.push(conn.getNode(id));
}
});
return nodes;
}
function getMembers(obj,conn) {
var members=[];
array.forEach(obj.childNodes,function(item) {
if (item.nodeName=='member') {
var id =getAttribute(item,'ref');
var type=getAttribute(item,'type');
var role=getAttribute(item,'role');
var obj=conn.getOrCreate(id,type);
members.push(new iD.RelationMember(obj,role));
}
});
return members;
}
},
});
// ----------------------------------------------------------------------
// End of module
});

45
js/iD/Controller.js Executable file
View File

@@ -0,0 +1,45 @@
// iD/Controller.js
define(['dojo/_base/declare','dojo/on','iD/actions/UndoStack'], function(declare,on){
// ----------------------------------------------------------------------
// Controller base class
declare("iD.Controller", null, {
state: null, // current ControllerState
map: null, // current Map
stepper: null, // current StepPane
undoStack: null, // main undoStack
constructor:function(_map) {
this.map=_map;
this.undoStack=new iD.actions.UndoStack();
},
setStepper:function(_stepper) {
this.stepper=_stepper;
},
setState:function(newState) {
if (newState==this.state) { return; }
if (this.state) {
this.state.exitState(newState);
on.emit(window, "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() });
},
entityMouseEvent:function(event,entityUI) {
if (!this.state) { return; }
var newState=this.state.processMouseEvent(event,entityUI);
this.setState(newState);
},
});
// ----------------------------------------------------------------------
// End of module
});

246
js/iD/Entity.js Executable file
View File

@@ -0,0 +1,246 @@
// iD/Entity.js
// Entity classes for iD
define(['dojo/_base/declare','dojo/_base/array',
'iD/actions/AddNodeToWayAction'
], function(declare,array){
// ----------------------------------------------------------------------
// Entity base class
declare("iD.Entity", null, {
connection: null,
id: NaN,
loaded: false,
tags: null,
entityType: '',
parents: null,
modified: false,
deleted: false,
constructor:function() {
this.tags={};
this.parents=new Hashtable();
},
isType:function(_type) {
return this.entityType==_type;
},
getTagsHash:function() {
return this.tags;
},
toString:function() {
return this.entityType+"."+this.id;
},
// Provoke redraw and other changes
refresh:function() { this.connection.refreshEntity(this); },
// Clean and dirty (only called from UndoableEntityAction)
markClean:function() { this.modified=false; },
markDirty:function() { this.modified=true; },
isDirty:function() { return this.modified; },
// Deletion
setDeletedState:function(isDeleted) { this.deleted=isDeleted; },
// Bounding box check (to be overridden)
within:function(left,right,top,bottom) { return !this.deleted; },
// Parent-handling
addParent:function(_entity) {
this.parents.put(_entity,true);
},
removeParent:function(_entity) {
this.parents.remove(_entity);
},
hasParent:function(_entity) {
return this.parents.containsKey(_entity);
},
parentObjects:function() {
return this.parents.keys();
},
hasParentWays:function() {
var p=this.parentObjects();
for (var i in p) {
if (p[i].entityType=='way') { return true; }
}
return false;
},
parentWays:function() {
return this.parentObjectsOfClass('way');
},
parentRelations:function() {
return this.parentObjectsOfClass('relation');
},
parentObjectsOfClass:function(_class) {
var p=this.parentObjects(), c=[];
for (var i in p) {
if (p[i].entityType==_class) { c.push(p[i]); }
}
return c;
},
// Halcyon also implements:
// removeFromParents()
// hasParents()
// findParentRelationsOfType(type,role)
// getRelationMemberships()
// countParentObjects(within)
// memberships()
});
// ----------------------------------------------------------------------
// Node class
declare("iD.Node", [iD.Entity], {
lat:NaN,
latp:NaN,
lon:NaN,
entityType:"node",
constructor:function(_conn,_id,_lat,_lon,_tags,_loaded) {
this.connection=_conn;
this.id=Number(_id);
this.lat=Number(_lat);
this.lon=Number(_lon);
this.tags=_tags;
this.loaded=(_loaded==undefined) ? true : _loaded;
this.project();
this.modified=this.id<0;
},
project:function() {
this.latp=180/Math.PI * Math.log(Math.tan(Math.PI/4+this.lat*(Math.PI/180)/2));
},
within:function(left,right,top,bottom) { return (this.lon>=left) && (this.lon<=right) && (this.lat>=bottom) && (this.lat<=top) && !this.deleted; },
});
// ----------------------------------------------------------------------
// Way class
declare("iD.Way", [iD.Entity], {
nodes: null,
entityType: "way",
edgel: NaN,
edger: NaN,
edget: NaN,
edgeb: NaN,
constructor:function(_conn,_id,_nodes,_tags,_loaded) {
this.connection=_conn;
this.id=Number(_id);
this.nodes=_nodes;
this.tags=_tags;
this.loaded=(_loaded==undefined) ? true : _loaded;
this.modified=this.id<0;
var w=this; array.forEach(_nodes,function(node) {
node.addParent(w);
});
this.calculateBbox();
},
length:function() {
return this.nodes.length;
},
isClosed:function() {
return this.nodes[this.nodes.length-1]==this.nodes[0];
},
isType:function(_type) {
switch (_type) {
case 'way': return true;
case 'area': return this.isClosed;
case 'line': return !(this.isClosed);
}
return false;
},
getNode:function(index) { return this.nodes[index]; },
getFirstNode:function() { return this.nodes[0]; },
getLastNode:function() { return this.nodes[this.nodes.length-1]; },
// Bounding-box handling
within:function(left,right,top,bottom) {
if (!this.edgel ||
(this.edgel<left && this.edger<left ) ||
(this.edgel>right && this.edger>right ) ||
(this.edgeb<bottom && this.edget<bottom) ||
(this.edgeb>top && this.edgeb>top ) || this.deleted) { return false; }
return true;
},
calculateBbox:function() {
this.edgel=999999; this.edger=-999999;
this.edgeb=999999; this.edget=-999999;
for (var i in this.nodes) { this.expandBbox(this.nodes[i]); }
},
expandBbox:function(node) {
this.edgel=Math.min(this.edgel,node.lon);
this.edger=Math.max(this.edger,node.lon);
this.edgeb=Math.min(this.edgeb,node.lat);
this.edget=Math.max(this.edget,node.lat);
},
// Action callers
doAppendNode:function(node, performAction) {
if (node!=this.getLastNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, -1, true));
return this.nodes.length + 1;
},
doPrependNode:function(node, performAction) {
if (node!=this.getFirstNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, 0, true));
return this.nodes.length + 1;
},
});
// ----------------------------------------------------------------------
// Relation class
declare("iD.Relation", [iD.Entity], {
members:null,
entityType:"relation",
constructor:function(_conn,_id,_members,_tags,_loaded) {
this.connection=_conn;
this.id=Number(_id);
this.members=_members;
this.tags=_tags;
this.modified=this.id<0;
this.loaded=(_loaded==undefined) ? true : _loaded;
var r=this; array.forEach(_members,function(member) {
member.entity.addParent(r);
});
},
});
// ----------------------------------------------------------------------
// RelationMember class
declare("iD.RelationMember", [], {
entity:null,
role:"",
constructor:function(_entity,_role) {
this.entity=_entity;
this.role=_role;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,63 @@
// iD/actions/AddNodeToWayAction.js
define(['dojo/_base/declare','iD/actions/UndoableEntityAction'], function(declare){
// ----------------------------------------------------------------------
// AddNodeToWayAction class
declare("iD.actions.AddNodeToWayAction", [iD.actions.UndoableEntityAction], {
node: null,
nodeList: null,
index: 0,
firstNode: null,
autoDelete: true,
constructor:function(_way, _node, _nodeList, _index, _autoDelete) {
this.entity = _way;
this.node = _node;
this.nodeList = _nodeList;
this.index = _index;
this.autoDelete = _autoDelete;
},
doAction:function() {
var way=this.entity; // shorthand
// undelete way if it was deleted before (only happens on redo)
if (way.deleted) {
way.setDeletedState(false);
if (!this.firstNode.hasParentWays()) this.firstNode.connection.unregisterPOI(firstNode);
this.firstNode.addParent(way);
}
// add the node
if (this.index==-1) this.index=this.nodeList.length;
this.node.addParent(way);
this.nodeList.splice(this.index, 0, this.node);
this.markDirty();
// way.expandBbox(this.node);
way.refresh();
return this.SUCCESS;
},
undoAction:function() {
var way=this.entity; // shorthand
if (this.autoDelete && way.length()==2 && way.parentRelations().length()) return this.FAIL;
// remove node
var removed=nodeList.splice(index, 1);
if (this.nodeList.indexOf(removed[0])==-1) { removed[0].removeParent(way); }
this.markClean();
way.refresh();
// ** if the way is now 1-length, we should do something like deleting it and
// converting the remaining node to a POI (see P2)
return this.SUCCESS;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,49 @@
// iD/actions/UndoableAction.js
define(['dojo/_base/declare','iD/actions/UndoableAction'], function(declare){
// ----------------------------------------------------------------------
// CreateEntityAction class
declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
setCreate:null,
deleteAction:null,
// When undo is called, instead of simply removing the entity, we work through
// to make a Delete[Entity]Action, call that, and store it for later
// Then, when this action is called again (i.e. a redo), instead of creating yet another entity, we call the deleteAction.undoAction
constructor:function(entity,setCreate) {
this.setCreate = setCreate;
this.setName("Create "+entity.entityType);
},
doAction:function() {
if (this.deleteAction!=null) {
this.deleteAction.undoAction(); // redo
} else {
this.setCreate(this.entity, false); // first time
}
this.markDirty();
return this.SUCCESS;
},
undoAction:function() {
// if the undo is called for the first time, call for a deletion, and (via setAction) store the
// deletion action for later. We'll undo the deletion if we get asked to redo this action
if (this.deleteAction==null) { this.entity.remove(this.setAction); }
this.deleteAction.doAction();
this.markClean();
return this.SUCCESS;
},
setAction:function(action) {
deleteAction = action;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,47 @@
// iD/actions/CreatePOIAction.js
define(['dojo/_base/declare','dojo/_base/lang','iD/actions/UndoableAction'], function(declare,lang){
// ----------------------------------------------------------------------
// CreatePOIAction class
declare("iD.actions.CreatePOIAction", [iD.actions.CompositeUndoableAction], {
newNode: null,
tags: null,
lat: NaN,
lon: NaN,
connection: null,
constructor:function(connection,tags,lat,lon) {
this.setName("Create POI");
this.connection = connection;
this.tags = tags;
this.lat = lat;
this.lon = lon;
},
doAction:function() {
if (this.newNode==null) {
this.newNode=this.connection.createNode(this.tags,this.lat,this.lon,lang.hitch(this,this.push));
}
this.inherited(arguments);
this.connection.registerPOI(this.newNode);
return this.SUCCESS;
},
undoAction:function() {
this.inherited(arguments);
this.connection.unregisterPOI(this.newNode);
return this.SUCCESS;
},
getNode:function() {
return this.newNode;
},
});
// ----------------------------------------------------------------------
// End of module
});

110
js/iD/actions/UndoStack.js Normal file
View File

@@ -0,0 +1,110 @@
// iD/actions/UndoStack.js
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// UndoStack base class
declare("iD.actions.UndoStack", null, {
undoActions: null,
redoActions: null,
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
constructor:function() {
this.undoActions=[];
this.redoActions=[];
},
addAction:function(_action) {
var result = _action.doAction();
switch (result) {
case this.FAIL:
// do something bad
break;
case this.NO_CHANGE:
break;
case this.SUCCESS:
default:
if (this.undoActions.length>0) {
var previous = this.undoActions[this.undoActions.length-1];
if (_action.mergePrevious(previous)) {
_action.wasDirty = previous.wasDirty;
_action.connectionWasDirty = previous.connectionWasDirty;
this.undoActions.pop();
}
}
this.undoActions.push(_action);
this.redoActions=[];
break;
}
},
breakUndo:function() {
this.undoActions = [];
this.redoActions = [];
},
canUndo:function() {
return this.undoActions.length > 0;
},
canRedo:function() {
return this.redoActions.length > 0;
},
// Undo the most recent action, and add it to the top of the redo stack
undo:function() {
if (!this.undoActions.length) { return; }
var action = undoActions.pop();
action.undoAction();
redoActions.push(action);
},
undoIfAction:function(_action) {
if (!this.undoActions.length) { return; }
if (this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undo();
return true;
}
return false;
},
removeLastIfAction:function(_action) {
if (this.undoActions.length && this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undoActions.pop();
}
},
getUndoDescription:function() {
if (this.undoActions.length==0) return null;
if (this.undoActions[this.undoActions.length-1].name) {
return this.undoActions[this.undoActions.length-1].name;
}
return null;
},
getRedoDescription:function() {
if (this.redoActions.length==0) return null;
if (this.redoActions[this.redoActions.length-1].name) {
return this.redoActions[this.redoActions.length-1].name;
}
return null;
},
// Takes the action most recently undone, does it, and adds it to the undo stack
redo:function() {
if (!this.redoActions.length) { return; }
var action = this.redoActions.pop();
action.doAction();
this.undoActions.push(action);
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,135 @@
// iD/actions/UndoableAction.js
// we don't currently do connectionWasDirty, and we should
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// UndoableAction base class
declare("iD.actions.UndoableAction", null, {
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
name: "",
constructor:function() {
},
doAction:function() {
return FAIL;
},
undoAction:function() {
return FAIL;
},
mergePrevious:function() {
return false;
},
setName:function(_name) {
this.name=_name;
},
});
// ----------------------------------------------------------------------
// UndoableEntityAction class
declare("iD.actions.UndoableEntityAction", [iD.actions.UndoableAction], {
wasDirty: false,
connectionWasDirty: false,
initialised: false,
entity: null,
constructor:function(_entity) {
this.entity=_entity;
},
markDirty:function() {
if (!this.initialised) { this.init(); }
if (!this.wasDirty) { this.entity.markDirty(); }
// if (!this.connectionWasDirty ) { this.entity.connection.markDirty(); }
},
markClean:function() {
if (!this.initialised) { this.init(); }
if (!this.wasDirty) { this.entity.markClean(); }
// if (!connectionWasDirty) { this.entity.connection.markClean(); }
},
// Record whether or not the entity and connection were clean before this action started
init:function() {
this.wasDirty = this.entity.isDirty;
// this.connectionWasDirty = this.entity.connection.isDirty;
this.initialised = true;
},
});
// ----------------------------------------------------------------------
// UndoableEntityAction class
declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
actions: null,
actionsDone: false,
constructor:function() {
this.actions=[];
},
push:function(action) {
this.actions.push(action);
},
clearActions:function() {
this.actions=[];
},
doAction:function() {
if (this.actionsDone) { return this.FAIL; }
var somethingDone=false;
for (var i=0; i<this.actions.length; i++) {
var action = this.actions[i];
var result = action.doAction();
switch (result) {
case this.NO_CHANGE:
// splice this one out as it doesn't do anything
this.actions.splice(i,1);
i--;
break;
case this.FAIL:
this.undoFrom(i);
return this.FAIL;
default:
somethingDone=true;
break;
}
}
this.actionsDone = true;
return somethingDone ? this.SUCCESS : this.NO_CHANGE;
},
undoAction:function() {
if (!this.actionsDone) { return this.FAIL; }
this.undoFrom(this.actions.length);
return this.SUCCESS;
},
undoFrom:function(index) {
for (var i=index-1; i>=0; i--) {
this.actions[i].undoAction();
}
this.actionsDone=false;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,45 @@
// iD/actions/UndoableEntityAction.js
define(['dojo/_base/declare','iD/actions/UndoableAction'], function(declare){
// ----------------------------------------------------------------------
// UndoableEntityAction class
declare("iD.actions.UndoableEntityAction", [iD.actions.UndoableAction], {
wasDirty: false,
connectionWasDirty: false,
initialised: false,
entity: null,
constructor:function(_entity) {
this.entity = _entity;
},
markDirty:function() {
if (!this.initialised) this.init();
if (!this.wasDirty) this.entity.markDirty();
if (!this.connectionWasDirty) this.entity.connection.markDirty();
},
markClean:function() {
if (!this.initialised) this.init();
if (!this.wasDirty) this.entity.markClean();
if (!this.connectionWasDirty) this.entity.connection.markClean();
},
// Record whether or not the entity and connection were clean before this action started
init:function() {
this.wasDirty = this.entity.isDirty();
this.connectionWasDirty = this.entity.connection.isDirty();
this.initialised = true;
},
toString:function() {
return this.name + " " + this.entity.entityType + " " + this.entity.id;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,49 @@
// iD/controller/ControllerState.js
define(['dojo/_base/declare','dojo/_base/lang'], function(declare,lang) {
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.ControllerState", null, {
controller: null, // parent Controller
constructor:function() {
},
setController:function(_controller) {
this.controller=_controller;
},
processMouseEvent:function(event,entityUI) {
},
enterState:function() {
},
exitState:function(newState) {
},
stateName:function() {
return this.stateNameAsArray.join('.');
},
stateNameAsArray:function() {
return this.__proto__.declaredClass.split('.').slice(2);
},
getConnection:function() {
return this.controller.map.conn;
},
undoAdder:function() {
/* This is a convenient shorthand for adding an action to the global undo stack,
setting the scope correctly. */
return lang.hitch(this.controller.undoStack, this.controller.undoStack.addAction);
}
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,39 @@
// iD/controller/edit/NoSelection.js
define(['dojo/_base/declare',
'iD/controller/ControllerState',
'iD/controller/edit/SelectedWay',
'iD/controller/edit/SelectedWayNode',
'iD/controller/edit/SelectedPOINode',
], function(declare){
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.edit.NoSelection", [iD.controller.ControllerState], {
constructor:function() {
},
processMouseEvent:function(event,entityUI) {
this.inherited(arguments);
if (!entityUI) { return this; }
var entity=entityUI.entity;
if (event.type=='click') {
switch (entity.entityType) {
case 'node':
var ways=entity.parentWays();
if (ways.length==0) { return new iD.controller.edit.SelectedPOINode(entity); }
else { return new iD.controller.edit.SelectedWayNode(entity,ways[0]); }
case 'way':
return new iD.controller.edit.SelectedWay(entityUI.entity);
}
}
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,44 @@
// iD/controller/edit/SelectedPOINode.js
define(['dojo/_base/declare','iD/controller/ControllerState'], function(declare){
// ----------------------------------------------------------------------
// SelectedPOINode class
declare("iD.controller.edit.SelectedPOINode", [iD.controller.ControllerState], {
node: null,
nodeUI: null,
constructor:function(_node) {
this.node=_node;
},
enterState:function() {
this.nodeUI=this.controller.map.getUI(this.node);
this.nodeUI.setStateClass('selected');
this.nodeUI.redraw();
},
exitState:function() {
this.nodeUI.resetStateClass('selected');
this.nodeUI.redraw();
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
if (event.type=='click') {
switch (entityType) {
case null: return new iD.controller.edit.NoSelection();
case 'node': return new iD.controller.edit.SelectedPOINode(entityUI.entity);
case 'way': return new iD.controller.edit.SelectedWay(entityUI.entity);
}
}
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,53 @@
// iD/controller/edit/SelectedWay.js
define(['dojo/_base/declare','iD/controller/ControllerState'], function(declare){
// ----------------------------------------------------------------------
// SelectedWay class
declare("iD.controller.edit.SelectedWay", [iD.controller.ControllerState], {
way: null,
wayUI: null,
constructor:function(_way) {
this.way=_way;
},
enterState:function() {
this.wayUI=this.controller.map.getUI(this.way);
this.wayUI.setStateClass('selected');
this.wayUI.setStateClass('shownodes');
this.wayUI.redraw();
},
exitState:function() {
this.wayUI.resetStateClass('selected');
this.wayUI.resetStateClass('shownodes');
this.wayUI.redraw();
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
if (event.type=='click') {
switch (entityType) {
case null:
return new iD.controller.edit.NoSelection();
case 'node':
var ways=entity.parentWays();
if (entity.hasParent(this.way)) { return new iD.controller.edit.SelectedWayNode(entity,this.way); }
else if (ways.length==0) { return new iD.controller.edit.SelectedPOINode(entity); }
else { return new iD.controller.edit.SelectedWayNode(entity,ways[0]); }
case 'way':
return new iD.controller.edit.SelectedWay(entityUI.entity);
}
} else {
}
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,50 @@
// iD/controller/edit/SelectedWayNode.js
define(['dojo/_base/declare','iD/controller/ControllerState'], function(declare){
// ----------------------------------------------------------------------
// SelectedWayNode class
declare("iD.controller.edit.SelectedWayNode", [iD.controller.ControllerState], {
node: null,
way: null,
constructor:function(_node,_way) {
this.node=_node;
this.way=_way;
},
enterState:function() {
this.controller.map.getUI(this.way ).setStateClass('shownodes').redraw();
this.controller.map.getUI(this.node).setStateClass('selected' ).redraw();
},
exitState:function() {
this.controller.map.getUI(this.way ).resetStateClass('shownodes').redraw();
this.controller.map.getUI(this.node).resetStateClass('selected' ).redraw();
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
if (event.type=='click') {
switch (entityType) {
case null:
return new iD.controller.edit.NoSelection();
case 'node':
var ways=entity.parentWays();
if (entity.hasParent(this.way)) { return new iD.controller.edit.SelectedWayNode(entity,this.way); }
else if (ways.length==0) { return new iD.controller.edit.SelectedPOINode(entity); }
else { return new iD.controller.edit.SelectedWayNode(entity,ways[0]); }
case 'way':
return new iD.controller.edit.SelectedWay(entityUI.entity);
}
}
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,165 @@
// iD/controller/shape/DrawWay.js
/*
Add road or shape -> DrawWay
The user is drawing a way.
Goes to:
-> click empty area: adds point, continues
-> click way: adds junction, continues
-> click node: adds to way, continues
-> double-click, or click this way: goes to Edit/SelectedWay
*/
define(['dojo/_base/declare','dojo/_base/lang','dojox/gfx/shape','iD/controller/ControllerState'], function(declare,lang,shape){
// ----------------------------------------------------------------------
// DrawWay class
declare("iD.controller.shape.DrawWay", [iD.controller.ControllerState], {
way: null,
wayUI: null,
editEnd: false,
constructor:function(_way) {
this.way=_way;
},
enterState:function() {
this.wayUI=this.controller.map.getUI(this.way);
this.wayUI.setStateClass('selected');
this.wayUI.setStateClass('shownodes');
this.wayUI.redraw();
this.controller.stepper.highlight(2);
},
exitState:function() {
this.controller.map.clearElastic();
this.wayUI.resetStateClass('selected');
this.wayUI.resetStateClass('shownodes');
this.wayUI.redraw();
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
var map=this.controller.map;
if (event.type=='mouseover' && entityType=='way' && entityUI!=this.wayUI) {
// Mouse over way, show hover highlight
entityUI.setStateClass('shownodeshover');
entityUI.redraw();
this.wayUI.redraw();
this.updateElastic(event);
return this;
} else if (event.type=='mouseout' && entityType=='way' && entityUI!=this.wayUI) {
// Mouse left way, remove hover highlight
// Find what object we're moving into
var into=shape.byId((event.hasOwnProperty('toElement') ? event.toElement : event.relatedTarget).__gfxObject__);
// If it's a nodeUI that belongs to a hovering way, don't deselect
if (into && into.hasOwnProperty('source') && into.source.hasStateClass('hoverway') && into.source.entity.hasParent(entity)) { return this; }
entityUI.resetStateClass('shownodeshover');
entityUI.redraw();
this.wayUI.redraw();
this.updateElastic(event);
return this;
} else if (event.type=='mouseout' && entityType=='node') {
// Mouse left node, remove hover highlight from parent way too
var ways=entity.parentWays();
for (var i in ways) {
var ui=this.controller.map.getUI(ways[i]);
if (ui && ui.hasStateClass('shownodeshover')) {
ui.resetStateClass('shownodeshover');
ui.redraw();
}
}
this.updateElastic(event);
this.wayUI.redraw();
return this;
} else if (event.type=='mousemove') {
// Mouse moved, update elastic
this.updateElastic(event);
return this;
} else if (event.type=='mousedown') {
switch (entityType) {
case 'node':
// Click on node
if (entity==this.getDrawingNode()) {
// Double-click, so complete drawing
return new iD.controller.edit.SelectedWay(this.way);
} else if (entity==this.getStartNode()) {
// Start of this way, so complete drawing
this.appendNode(entity, this.undoAdder() );
return new iD.controller.edit.SelectedWay(this.way);
} else {
// Add to way
this.appendNode(entity, this.undoAdder() );
return this;
}
case 'way':
// Click on way, add new junction node to way
console.log("clicked a way, add new junction to way");
var ways=[entity]; // ** needs to find all the ways under the mouse
var undo=new iD.actions.CompositeUndoableAction();
var node=this.appendNewNode(event, undo);
// array.forEach(ways, function(w) { w.insertNodeAtClosestPosition(node, true, undo.push); } );
var action=this.undoAdder(); action(undo);
return this;
}
} else if (event.type=='click') {
// Click on empty space, add new node to way
console.log("clicked empty space, add a new node to way");
var undo=new iD.actions.CompositeUndoableAction();
this.appendNewNode(event, undo);
var action=this.undoAdder(); action(undo);
return this;
}
return this;
},
updateElastic:function(event) {
var map=this.controller.map;
map.drawElastic(
map.lon2coord(this.getDrawingNode().lon),
map.lat2coord(this.getDrawingNode().lat),
map.mouseX(event), map.mouseY(event)
);
},
getDrawingNode:function() {
return (this.editEnd ? this.way.nodes[this.way.length()-1] : this.way.nodes[0]);
},
getStartNode:function() {
return (this.editEnd ? this.way.nodes[0] : this.way.nodes[this.way.length()-1]);
},
appendNode:function(node, performAction) {
if (this.editEnd) { this.way.doAppendNode(node, performAction); }
else { this.way.doPrependNode(node, performAction); }
},
appendNewNode:function(event, undo) {
var map=this.controller.map;
var node=this.getConnection().createNode(
{},
map.coord2lat(map.mouseY(event)),
map.coord2lon(map.mouseX(event)), lang.hitch(undo,undo.push) );
this.appendNode(node, lang.hitch(undo,undo.push));
return node;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,78 @@
// iD/controller/shape/NoSelection.js
/*
Add road or shape -> NoSelection
The user has clicked 'Add road or shape', but hasn't yet started drawing.
Goes to:
-> click empty area: goes to shape/DrawWay
-> click way: goes to shape/SelectedWay
-> click way-node: goes to shape/SelectedWayNode
-> click POI: ** not done yet, needs to ask "convert to shape"?
*/
define(['dojo/_base/declare','dojo/_base/lang',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
'iD/controller/shape/DrawWay',
'iD/controller/shape/SelectedWay',
'iD/controller/shape/SelectedWayNode',
'iD/controller/shape/SelectedPOINode',
], function(declare,lang){
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
constructor:function() {
},
enterState:function() {
this.controller.stepper.setSteps([
"Click anywhere on the map to start drawing there",
"Keep clicking to add each point, and press Enter or double-click when you're done",
"Set the type of the road or shape"
]).highlight(1);
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
var map=this.controller.map;
if (event.type=='click') {
switch (entityType) {
case 'node':
// Click to select a node
var ways=entity.parentWays();
if (ways.length==0) { return new iD.controller.shape.SelectedPOINode(entity); }
else { return new iD.controller.shape.SelectedWayNode(entity,ways[0]); }
case 'way':
// Click to select a way
return new iD.controller.shape.SelectedWay(entityUI.entity);
default:
// Click to start a new way
var undo = new iD.actions.CompositeUndoableAction();
console.log("Event is ",event.type);
var startNode = this.getConnection().createNode(
{},
map.coord2lat(map.mouseY(event)),
map.coord2lon(map.mouseX(event)), lang.hitch(undo,undo.push) );
var way = this.getConnection().createWay({}, [startNode], lang.hitch(undo,undo.push) );
this.controller.undoStack.addAction(undo);
this.controller.map.createUI(way);
console.log("Started new way");
return new iD.controller.shape.DrawWay(way);
}
}
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,34 @@
// iD/controller/shape/SelectedPOINode.js
/*
Add road or shape -> SelectedPOINode
The user has clicked 'Add road or shape', then a POI node to be converted to a way.
*/
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
], function(declare){
// ----------------------------------------------------------------------
// SelectedPOINode class
declare("iD.controller.shape.SelectedPOINode", [iD.controller.ControllerState], {
constructor:function() {
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,34 @@
// iD/controller/shape/SelectedWay.js
/*
Add road or shape -> SelectedWay
The user has clicked 'Add road or shape', then a way to start the new way at.
*/
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
], function(declare){
// ----------------------------------------------------------------------
// SelectedWayNode class
declare("iD.controller.shape.SelectedWay", [iD.controller.ControllerState], {
constructor:function() {
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,34 @@
// iD/controller/shape/SelectedWayNode.js
/*
Add road or shape -> SelectedWayNode
The user has clicked 'Add road or shape', then a way-node to start the way at.
*/
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
], function(declare){
// ----------------------------------------------------------------------
// SelectedWayNode class
declare("iD.controller.shape.SelectedWayNode", [iD.controller.ControllerState], {
constructor:function() {
},
processMouseEvent:function(event,entityUI) {
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
return this;
},
});
// ----------------------------------------------------------------------
// End of module
});

116
js/iD/renderer/EntityUI.js Executable file
View File

@@ -0,0 +1,116 @@
// iD/renderer/EntityUI.js
// EntityUI classes for iD
// multipolygon support - http://mail.dojotoolkit.org/pipermail/dojo-interest/2011-January/052042.html
// support 'interactive'
// line decoration, dots etc.
// fill images
// opacity
define(['dojo/_base/declare','dojo/_base/lang','iD/Entity','iD/renderer/Map'], function(declare,lang){
// ----------------------------------------------------------------------
// EntityUI base class
declare("iD.renderer.EntityUI", null, {
entity:null, // Entity this represents
map:null, // the Map object containing this
layer:0, // OSM layer
sprites:null, // array of sprites created for this EntityUI
styleList:null, // current StyleList
stateClasses:null, // list of stateClass tags to apply
constructor:function(_entity,_map,_stateClasses) {
this.entity=_entity;
this.map=_map;
this.stateClasses=_stateClasses ? _stateClasses.slice() : [];
this.sprites=[];
},
getConnection:function() {
return this.map.conn;
},
targetGroup:function(groupType,sublayer) {
return this.map.sublayer(this.layer,groupType,sublayer);
},
recordSprite:function(sprite) {
if (this.sprites.indexOf(sprite)==-1) { this.sprites.push(sprite); }
return sprite;
},
removeSprites:function() {
for (var i=0; i<this.sprites.length; i++) {
this.sprites[i].removeShape();
}
this.sprites=[];
},
refreshStyleList:function(tags) {
if (!this.styleList || !this.styleList.isValidAt(this.map.scale)) {
this.styleList=this.map.ruleset.getStyles(this.entity,tags,this.map.scale);
}
this.layer=this.styleList.layerOverride();
if (isNaN(this.layer)) {
this.layer=0;
if (tags['layer']) { this.layer=Number(tags['layer']); }
}
// Iterate through each subpart, drawing any styles on that layer
var drawn=false;
for (i=0; i<this.styleList.subparts.length; i++) {
var subpart=this.styleList.subparts[i];
}
},
getEnhancedTags:function() {
// Clone entity tags
var tags=lang.clone(this.entity.tags);
// Apply stateClasses (hover, selected, hoverway, selectedway)
for (var i in this.stateClasses) {
tags[':'+this.stateClasses[i]]='yes';
}
// todo - Add any common 'special-case' tags, e.g. :hasTags
return tags;
},
// State class handling
// Set all state classes at once, and prompt a redraw if they're different to previously
setStateClasses:function(_stateClasses) {
if (_stateClasses && this.stateClasses.join(',')!=_stateClasses.join(',')) {
this.stateClasses=_stateClasses.slice();
this.invalidateStyleList();
}
return this;
},
// Set a single state class, and prompt a redraw if it wasn't set previously
setStateClass:function(sc) {
if (this.stateClasses.indexOf(sc)==-1) {
this.stateClasses.push(sc);
this.invalidateStyleList();
}
return this;
},
// Reset a single state class, and prompt a redraw if it was set previously
resetStateClass:function(sc) {
if (this.stateClasses.indexOf(sc)>-1) {
this.stateClasses.splice(this.stateClasses.indexOf(sc),1);
this.invalidateStyleList();
}
return this;
},
hasStateClass:function(sc) {
return this.stateClasses.indexOf(sc)>-1;
},
invalidateStyleList:function() {
this.styleList=null;
},
// Mouse event handling
entityMouseEvent:function(event) {
this.map.controller.entityMouseEvent(event, event.gfxTarget.source);
event.stopPropagation();
},
});
// ----------------------------------------------------------------------
// End of module
});

448
js/iD/renderer/Map.js Executable file
View File

@@ -0,0 +1,448 @@
// iD/renderer/Map.js
// at present this combines P2's Map and MapPaint functionality
define(['dojo/_base/declare','dojo/_base/array','dojo/_base/event','dojo/_base/lang',
'dojo/dom-geometry',
'dojox/gfx','dojox/gfx/matrix',
'iD/Connection','iD/Entity','iD/renderer/EntityUI','iD/renderer/WayUI','iD/renderer/NodeUI'],
function(declare,array,Event,lang,domGeom,Gfx,Matrix){
// ----------------------------------------------------------------------
// Connection base class
declare("iD.renderer.Map", null, {
MASTERSCALE: 5825.4222222222,
MINSCALE: 14,
MAXSCALE: 23,
scale: NaN,
scalefactor: NaN,
baselon: NaN, // original longitude at top left of viewport
baselat: NaN, // original latitude at top left of viewport
baselatp: NaN, // original projected latitude at top left of viewport
div: '', // <div> of this map
surface: null, // <div>.surface containing the rendering
container: null, // root-level group within the surface
backdrop: null, // coloured backdrop (MapCSS canvas element)
conn: null, // data store
controller: null, // UI controller
nodeuis: {}, // graphic representations of data
wayuis: {}, // |
tilegroup: null, // group within container for adding bitmap tiles
tiles: [], // index of tile objects
tilebaseURL: 'http://ecn.t0.tiles.virtualearth.net/tiles/a$quadkey.jpeg?g=587&mkt=en-gb&n=z', // Bing imagery URL
dragging: false, // current drag state
dragged: false, // was most recent click a drag?
dragx: NaN, // click co-ordinates at previously recorded drag event
dragy: NaN, // |
startdragx: NaN, // click co-ordinates at start of drag
startdragy: NaN, // |
dragtime: NaN, // timestamp of mouseup (compared to stop resulting click from firing)
dragconnect: null, // event listener for endDrag
containerx: 0, // screen co-ordinates of container
containery: 0, // |
centrelat: NaN, // lat/long and bounding box of map
centrelon: NaN, // |
edgel: NaN, // |
edger: NaN, // |
edget: NaN, // |
edgeb: NaN, // |
mapheight: NaN, // size of map object in pixels
mapwidth: NaN, // |
layers: null, // array-like object of Groups, one for each OSM layer
minlayer: -5, // minimum OSM layer supported
maxlayer: 5, // maximum OSM layer supported
elastic: null, // Group for drawing elastic band
ruleset: null, // map style
// Constructor
constructor:function(_lat,_lon,_scale,_divname,_conn) {
// Bounds ** FIXME: shouldn't be hardcoded!
this.mapwidth=800;
this.mapheight=400;
// Initialise variables
this.nodeuis={},
this.wayuis={},
this.div=document.getElementById(_divname);
this.surface=Gfx.createSurface(_divname, this.mapwidth, this.mapheight);
this.backdrop=this.surface.createRect( { x:0, y:0, width: this.mapwidth, height: this.mapheight }).setFill(new dojo.Color([255,255,245,1]));
this.tilegroup=this.surface.createGroup();
this.container=this.surface.createGroup();
this.conn=_conn;
this.scale=_scale;
this.baselon=_lon;
this.baselat=_lat;
this.baselatp=this.lat2latp(_lat);
this.setScaleFactor();
this.updateCoordsFromViewportPosition();
// Initialise layers
this.layers={};
for (var l=this.minlayer; l<=this.maxlayer; l++) {
var r=this.container.createGroup();
this.layers[l]={
root: r,
fill: r.createGroup(),
casing: r.createGroup(),
stroke: r.createGroup(),
text: r.createGroup(),
hit: r.createGroup()
};
}
// Create group for elastic band
this.elastic = this.container.createGroup();
// Make draggable
this.backdrop.connect("onmousedown", lang.hitch(this,"startDrag"));
this.tilegroup.connect("onmousedown", lang.hitch(this,"startDrag"));
this.surface.connect("onclick", lang.hitch(this,"clickSurface"));
this.surface.connect("onmousemove", lang.hitch(this,"processMove"));
this.surface.connect("onmousedown", lang.hitch(this,"mouseEvent"));
this.surface.connect("onmouseup", lang.hitch(this,"mouseEvent"));
},
setController:function(_controller) {
this.controller=_controller;
},
// Supplementary method for gfx - moveToPosition
// This should ideally be core Dojo stuff: see http://bugs.dojotoolkit.org/ticket/15296
moveToPosition:function(group,position) {
var parent=group.getParent();
if (!parent) { return; }
this.moveChildToPosition(parent,group,position);
if (position==group.rawNode.parentNode.childNodes.length) {
group.rawNode.parentNode.appendChild(group.rawNode);
} else {
group.rawNode.parentNode.insertBefore(group.rawNode, group.rawNode.parentNode.childNodes[position]);
}
},
moveChildToPosition: function(parent,child,position) {
for(var i = 0; i < parent.children.length; ++i){
if(parent.children[i] == child){
parent.children.splice(i, 1);
parent.children.splice(position, 0, child);
break;
}
}
},
// Sprite and EntityUI handling
sublayer:function(layer,groupType,sublayer) {
// Sublayers are only implemented for stroke and fill
var collection=this.layers[layer][groupType];
switch (groupType) {
case 'casing':
case 'text':
case 'hit':
return collection;
}
// Find correct sublayer, inserting if necessary
var insertAt=collection.children.length;
for (var i=0; i<collection.children.length; i++) {
var sub=collection.children[i];
if (sub.sublayer==sublayer) { return sub; }
else if (sub.sublayer>sublayer) {
sub=collection.createGroup();
this.moveToPosition(sub,i);
sub.sublayer=sublayer;
return sub;
}
}
sub=collection.createGroup().moveToFront();
sub.sublayer=sublayer;
return sub;
},
createUI:function(entity,stateClasses) {
var id=entity.id;
switch (entity.entityType) {
case 'node':
if (!this.nodeuis[id]) { this.nodeuis[id]=new iD.renderer.NodeUI(entity,this,stateClasses); }
else { this.nodeuis[id].setStateClasses(stateClasses).redraw(); }
return this.nodeuis[id];
case 'way':
if (!this.wayuis[id]) { this.wayuis[id]=new iD.renderer.WayUI(entity,this,stateClasses); }
else { this.wayuis[id].setStateClasses(stateClasses).redraw(); }
return this.wayuis[id];
}
},
getUI:function(entity) {
switch (entity.entityType) {
case 'node': return this.nodeuis[entity.id];
case 'way': return this.wayuis[entity.id];
}
return null;
},
refreshUI:function(entity) {
switch (entity.entityType) {
case 'node': if (this.nodeuis[entity.id]) { this.nodeuis[entity.id].redraw(); } break;
case 'way': if (this.wayuis[entity.id] ) { this.wayuis[entity.id].redraw(); } break;
}
},
deleteUI:function(entity) {
switch (entity.entityType) {
case 'node': if (this.nodeuis[entity.id]) { this.nodeuis[entity.id].removeSprites(); delete this.nodeuis[entity.id]; } break;
case 'way': if (this.wayuis[entity.id] ) { this.wayuis[entity.id].removeSprites(); delete this.wayuis[entity.id]; } break;
}
},
// Ask connection to load data
download:function() {
this.conn.loadFromAPI(this.edgel, this.edger, this.edget, this.edgeb);
},
// Draw/refresh all EntityUIs within the bbox, and remove any others
updateUIs:function(redraw,remove) {
var m = this;
var way, poi;
var o = this.conn.getObjectsByBbox(this.edgel,this.edger,this.edget,this.edgeb);
array.forEach(o.waysInside, function(way) {
if (!way.loaded) return;
if (!m.wayuis[way.id]) { m.createUI(way); }
else if (redraw) { m.wayuis[way.id].recalculate(); m.wayuis[way.id].redraw(); }
});
if (remove) {
array.forEach(o.waysOutside, function(way) {
if (m.wayuis[way.id]) { // && !m.wayuis[way.id].purgable
if (redraw) { m.wayuis[way.id].recalculate(); m.wayuis[way.id].redraw(); }
} else { m.deleteUI(way); }
});
}
array.forEach(o.poisInside, function(poi) {
if (!poi.loaded) return;
if (!m.nodeuis[poi.id]) { m.createUI(poi); }
else if (redraw) { m.nodeuis[poi.id].redraw(); }
});
if (remove) {
array.forEach(o.poisOutside, function(poi) {
if (m.nodeuis[poi.id]) { // && !m.nodeuis[poi.id].purgable
if (redraw) { m.nodeuis[poi.id].redraw(); }
} else { m.deleteUI(poi); }
});
}
},
// Zoom handling
zoomIn:function() {
if (this.scale!=this.MAXSCALE) { this.changeScale(this.scale+1); }
},
zoomOut:function() {
if (this.scale!=this.MINSCALE) { this.changeScale(this.scale-1); }
this.download();
},
changeScale:function(_scale) {
this.scale=_scale;
this.setScaleFactor();
this.blankTiles();
this.updateCoordsFromLatLon(this.centrelat,this.centrelon); // recentre
this.updateUIs(true,true);
},
setScaleFactor:function() {
this.scalefactor=this.MASTERSCALE/Math.pow(2,13-this.scale);
},
// Elastic band redrawing
clearElastic:function() {
this.elastic.clear();
},
drawElastic:function(x1,y1,x2,y2) {
this.elastic.clear();
// **** Next line is SVG-specific
this.elastic.rawNode.setAttribute("pointer-events","none");
this.elastic.createPolyline( [{ x:x1, y:y1 }, { x:x2, y:y2 }] ).setStroke( {
color: [0,0,0,1],
style: 'Solid',
width: 1 });
},
// Tile handling
// ** FIXME: needs to have configurable URLs
// ** FIXME: needs Bing attribution/logo etc.
// ** FIXME: needs to be nudgable
loadTiles:function() {
var tile_l=this.lon2tile(this.edgel);
var tile_r=this.lon2tile(this.edger);
var tile_t=this.lat2tile(this.edget);
var tile_b=this.lat2tile(this.edgeb);
for (var x=tile_l; x<=tile_r; x++) {
for (var y=tile_t; y<=tile_b; y++) {
if (!this.getTile(this.scale,x,y)) { this.fetchTile(this.scale,x,y); }
}
}
},
fetchTile:function(z,x,y) {
var t=this.tilegroup.createImage({
x: this.lon2coord(this.tile2lon(x)),
y: this.lat2coord(this.tile2lat(y)),
width: 256, height: 256,
src: this.tileURL(z,x,y)
});
this.assignTile(z,x,y,t);
},
getTile:function(z,x,y) {
if (this.tiles[z]==undefined) { return undefined; }
if (this.tiles[z][x]==undefined) { return undefined; }
return this.tiles[z][x][y];
},
assignTile:function(z,x,y,t) {
if (this.tiles[z]==undefined) { this.tiles[z]=[]; }
if (this.tiles[z][x]==undefined) { this.tiles[z][x]=[]; }
this.tiles[z][x][y]=t;
},
tileURL:function(z,x,y) {
var u='';
for (var zoom=z; zoom>0; zoom--) {
var byte=0;
var mask=1<<(zoom-1);
if ((x & mask)!=0) byte++;
if ((y & mask)!=0) byte+=2;
u=u+byte.toString();
}
return this.tilebaseURL.replace('$z',z).replace('$x',x).replace('$y',y).replace('$quadkey',u);
},
blankTiles:function() {
this.tilegroup.clear();
this.tiles=[];
},
// Co-ordinate management, dragging and redraw
startDrag:function(e) {
var srcElement = (e.gfxTarget==this.backdrop) ? e.gfxTarget : e.gfxTarget.parent;
Event.stop(e);
this.dragging=true;
this.dragged=false;
this.dragx=this.dragy=NaN;
this.startdragx=e.clientX;
this.startdragy=e.clientY;
this.dragconnect=srcElement.connect("onmouseup", lang.hitch(this,"endDrag"));
},
endDrag:function(e) {
Event.stop(e);
dojo.disconnect(this.dragconnect);
this.dragging=false;
this.dragtime=e.timeStamp;
this.updateCoordsFromViewportPosition();
if (Math.abs(e.clientX-this.startdragx)<3 && Math.abs(e.clientY-this.startdragy)<3) { return; }
this.download();
},
processMove:function(e) {
var x=e.clientX;
var y=e.clientY;
if (this.dragging) {
if (this.dragx) {
this.containerx+=(x-this.dragx);
this.containery+=(y-this.dragy);
this.updateOrigin();
this.dragged=true;
}
this.dragx=x;
this.dragy=y;
} else {
this.controller.entityMouseEvent(e,null);
}
},
// Tell Dojo to update the viewport origin
updateOrigin:function() {
this.container.setTransform([Matrix.translate(this.containerx,this.containery)]);
this.tilegroup.setTransform([Matrix.translate(this.containerx,this.containery)]);
},
mouseEvent:function(e) {
// If the user mouses down within the fill of a shape, start the drag
if (e.type=='mousedown') { this.startDrag(e); }
// ** FIXME: we may want to reinstate this at some point...
// this.controller.entityMouseEvent(e,null);
},
// Update centre and bbox from the current viewport origin
updateCoordsFromViewportPosition:function(e) {
this.updateCoords(this.containerx,this.containery);
},
// Update centre and bbox to a specified lat/lon
updateCoordsFromLatLon:function(lat,lon) {
this.updateCoords(-(this.lon2coord(lon)-this.mapwidth/2),
-(this.lat2coord(lat)-this.mapheight/2));
},
// Set centre and bbox, called from the above methods
updateCoords:function(x,y) {
this.containerx=x; this.containery=y; this.updateOrigin();
this.centrelon=this.coord2lon(-x + this.mapwidth/2);
this.centrelat=this.coord2lat(-y + this.mapheight/2);
this.edget=this.coord2lat(-y);
this.edgeb=this.coord2lat(-y + this.mapheight);
this.edgel=this.coord2lon(-x);
this.edger=this.coord2lon(-x + this.mapwidth);
this.loadTiles();
},
clickSurface:function(e) {
if (this.dragged && e.timeStamp==this.dragtime) { return; }
this.controller.entityMouseEvent(e,null);
},
latp2coord:function(a) { return -(a-this.baselatp)*this.scalefactor; },
coord2latp:function(a) { return a/-this.scalefactor+this.baselatp; },
lon2coord:function(a) { return (a-this.baselon)*this.scalefactor; },
coord2lon:function(a) { return a/this.scalefactor+this.baselon; },
lat2latp:function(a) { return 180/Math.PI * Math.log(Math.tan(Math.PI/4+a*(Math.PI/180)/2)); },
latp2lat:function(a) { return 180/Math.PI * (2 * Math.atan(Math.exp(a*Math.PI/180)) - Math.PI/2); },
lat2coord:function(a) { return -(this.lat2latp(a)-this.baselatp)*this.scalefactor; },
coord2lat:function(a) { return this.latp2lat(a/-this.scalefactor+this.baselatp); },
lon2tile:function(a) { return (Math.floor((a+180)/360*Math.pow(2,this.scale))); },
lat2tile:function(a) { return (Math.floor((1-Math.log(Math.tan(a*Math.PI/180) + 1/Math.cos(a*Math.PI/180))/Math.PI)/2 *Math.pow(2,this.scale))); },
tile2lon:function(a) { return (a/Math.pow(2,this.scale)*360-180); },
tile2lat:function(a) {
var n=Math.PI-2*Math.PI*a/Math.pow(2,this.scale);
return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
},
// Turn event co-ordinates into map co-ordinates
mouseX:function(e) { return e.clientX - domGeom.getMarginBox(this.div).l - this.containerx; },
mouseY:function(e) { return e.clientY - domGeom.getMarginBox(this.div).t - this.containery; },
});
// ----------------------------------------------------------------------
// End of module
});

79
js/iD/renderer/NodeUI.js Executable file
View File

@@ -0,0 +1,79 @@
// iD/renderer/NodeUI.js
// NodeUI classes for iD
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/array','dojox/gfx/_base','iD/renderer/EntityUI'],
function(declare,lang,array,g){
// ----------------------------------------------------------------------
// NodeUI class
declare("iD.renderer.NodeUI", [iD.renderer.EntityUI], {
constructor:function() {
this.redraw();
},
getEnhancedTags:function() {
var tags=this.inherited(arguments);
if (!this.entity.hasParentWays()) { tags[':poi']='yes'; }
// add junction and dupe
return tags;
},
redraw:function() {
var node=this.entity;
this.removeSprites();
// Tags, position and styleList
var x=this.map.lon2coord(this.entity.lon);
var y=this.map.latp2coord(this.entity.latp);
var tags=this.getEnhancedTags();
this.refreshStyleList(tags);
// Iterate through each subpart, drawing any styles on that layer
var drawn=false;
var s,p,t,w,h;
for (i=0; i<this.styleList.subparts.length; i++) {
var subpart=this.styleList.subparts[i];
p = this.styleList.pointStyles[subpart];
if (!p || !p.drawn()) { continue; }
s = this.styleList.shapeStyles[subpart];
t = this.styleList.textStyles[subpart];
w = p.icon_width ? p.icon_width : 16;
h = p.icon_height ? p.icon_height: w;
// Draw icon
var shape;
switch (p.icon_image) {
case 'square': shape=this.targetGroup('stroke',p.sublayer).createRect({ x:x-w/2, y:y-h/2, width:w, height:h }); break;
case 'circle': shape=this.targetGroup('stroke',p.sublayer).createCircle({ cx:x, cy:y, r:w }); break;
default: shape=this.targetGroup('stroke',p.sublayer).createImage({ width:w, height:h, x: x-w/2, y: y-h/2, src:p.icon_image }); break;
}
switch (p.icon_image) {
case 'square':
case 'circle': shape.setStroke(s.shapeStrokeStyler()).setFill(s.shapeFillStyler()); break;
}
this.recordSprite(shape);
// Add text label
// Add hit-zone
var hit;
switch (p.icon_image) {
case 'circle': hit=this.targetGroup('hit').createCircle({ cx:x, cy:y, r:w }); break;
default: hit=this.targetGroup('hit').createRect({ x:x-w/2, y:y-h/2, width:w, height: h}); break;
}
hit.setFill([0,1,0,0]).setStroke( { width:2, color:[0,0,0,0] } );
this.recordSprite(hit);
hit.source=this;
hit.connect("onclick" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmousedown" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseup" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseenter", lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseleave", lang.hitch(this,this.entityMouseEvent));
}
},
});
// ----------------------------------------------------------------------
// End of module
});

119
js/iD/renderer/WayUI.js Executable file
View File

@@ -0,0 +1,119 @@
// iD/renderer/WayUI.js
// WayUI classes for iD
// multipolygon support - http://mail.dojotoolkit.org/pipermail/dojo-interest/2011-January/052042.html
// support 'interactive'
// line decoration, dots etc.
// fill images
// opacity
define(['dojo/_base/declare','dojo/_base/lang','iD/renderer/EntityUI'], function(declare,lang){
// ----------------------------------------------------------------------
// WayUI class
declare("iD.renderer.WayUI", [iD.renderer.EntityUI], {
constructor:function() {
this.redraw();
},
getEnhancedTags:function() {
var tags=this.inherited(arguments);
if (this.entity.isClosed()) { tags[':area']='yes'; }
return tags;
},
recalculate:function() {
// ** FIXME: todo
},
redraw:function() {
var way=this.entity;
var maxwidth=4;
var i;
this.removeSprites();
if (way.length()==0) { return; }
// Create tags and calculate styleList
var tags=this.getEnhancedTags();
this.refreshStyleList(tags);
// List of co-ordinates
var coords=[];
for (i=0; i<way.nodes.length; i++) {
var node=way.nodes[i];
coords.push( { x: this.map.lon2coord(node.lon), y: this.map.latp2coord(node.latp) } );
}
// Iterate through each subpart, drawing any styles on that layer
var drawn=false;
for (i=0; i<this.styleList.subparts.length; i++) {
var subpart=this.styleList.subparts[i];
if (this.styleList.shapeStyles[subpart]) {
var s=this.styleList.shapeStyles[subpart];
// Stroke
if (s.width) {
this.recordSprite(this.targetGroup('stroke',s.sublayer).createPolyline(coords).setStroke(s.strokeStyler()));
maxwidth=Math.max(maxwidth,s.width);
drawn=true;
}
// Fill
if (!isNaN(s.fill_color)) {
this.recordSprite(this.targetGroup('fill',s.sublayer).createPolyline(coords).setFill(s.fillStyler()));
drawn=true;
}
// Casing
if (s.casing_width) {
this.recordSprite(this.targetGroup('casing').createPolyline(coords).setStroke(s.casingStyler()));
maxwidth=Math.max(maxwidth,s.width+s.casing_width*2);
drawn=true;
}
}
// Text label on path
if (this.styleList.textStyles[subpart]) {
var t=this.styleList.textStyles[subpart];
if (t.text && tags[t.text]) {
var tp=this.recordSprite(this.targetGroup('text')
.createTextPath(t.textStyler(tags[t.text]))
.setFont(t.fontStyler())
.setFill(t.fillStyler())
.moveTo(coords[0].x,coords[0].y));
for (var j=1; j<coords.length; j++) {
tp.lineTo(coords[j].x,coords[j].y);
}
// *** this next line is SVG-specific
tp.rawNode.setAttribute("pointer-events","none");
}
}
}
// Add hitzone sprite
if (drawn) {
var hit=this.recordSprite(this.targetGroup('hit').createPolyline(coords).setStroke( { width:maxwidth+8, color: [0,0,0,0] } ));
hit.source=this;
hit.connect("onclick" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmousedown" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseup" , lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseenter", lang.hitch(this,this.entityMouseEvent));
hit.connect("onmouseleave", lang.hitch(this,this.entityMouseEvent));
}
// Draw nodes
for (i=0; i<way.length(); i++) {
var node=way.nodes[i];
var sc=[];
if (tags[':shownodes']) { sc.push('selectedway'); }
if (tags[':shownodeshover']) { sc.push('hoverway'); }
if (node.parentWays().length>1) { sc.push('junction'); }
this.map.createUI(node,sc);
}
},
entityMouseEvent:function(event) {
this.inherited(arguments);
},
});
// ----------------------------------------------------------------------
// End of module
});

44
js/iD/styleparser/Condition.js Executable file
View File

@@ -0,0 +1,44 @@
// iD/styleparser/Condition.js
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// Condition base class
declare("iD.styleparser.Condition", null, {
type: '', // eq/ne/regex etc.
params: [], // what to test against
constructor:function(_type) {
this.type =_type;
this.params=Array.prototype.slice.call(arguments,1);
},
test:function(tags) {
var p=this.params;
switch (this.type) {
case 'eq': return (tags[p[0]]==p[1]); break;
case 'ne': return (tags[p[0]]!=p[1]); break;
case 'regex': var r=new RegExp(p[1],"i");
return (r.test(tags[p[0]])); break;
case 'true': return (tags[p[0]]=='true' || tags[p[0]]=='yes' || tags[p[0]]=='1'); break;
case 'false': return (tags[p[0]]=='false' || tags[p[0]]=='no' || tags[p[0]]=='0'); break;
case 'set': return (tags[p[0]]!=undefined && tags[p[0]]!=''); break;
case 'unset': return (tags[p[0]]==undefined || tags[p[0]]==''); break;
case '<': return (Number(tags[p[0]])< Number(p[1])); break;
case '<=': return (Number(tags[p[0]])<=Number(p[1])); break;
case '>': return (Number(tags[p[0]])> Number(p[1])); break;
case '>=': return (Number(tags[p[0]])>=Number(p[1])); break;
}
return false;
},
toString:function() {
return "["+this.type+": "+this.params+"]";
},
});
// ----------------------------------------------------------------------
// End of module
});

54
js/iD/styleparser/Rule.js Executable file
View File

@@ -0,0 +1,54 @@
// iD/styleparser/Rule.js
/** A MapCSS selector. Contains a list of Conditions; the entity type to which the selector applies;
and the zoom levels at which it is true. way[waterway=river][boat=yes] would be parsed into one Rule.
The selectors and declaration together form a StyleChooser. */
define(['dojo/_base/declare','dojo/_base/array'], function(declare,array){
// ----------------------------------------------------------------------
// Rule base class
declare("iD.styleparser.Rule", null, {
conditions: [], // the Conditions to be evaluated for the Rule to be fulfilled
isAnd: true, // do all Conditions need to be true for the Rule to be fulfilled? (Always =true for MapCSS)
minZoom: 0, // minimum zoom level at which the Rule is fulfilled
maxZoom: 255, // maximum zoom level at which the Rule is fulfilled
subject: '', // entity type to which the Rule applies: 'way', 'node', 'relation', 'area' (closed way) or 'line' (unclosed way)
constructor:function(_subject) {
this.subject=_subject;
this.conditions=[];
},
addCondition:function(_condition) {
this.conditions.push(_condition);
},
/** Evaluate the Rule on the given entity, tags and zoom level.
Return true if the Rule passes, false if the conditions aren't fulfilled. */
test:function(entity,tags,zoom) {
if (this.subject!='' && !entity.isType(this.subject)) { return false; }
if (zoom<this.minZoom || zoom>this.maxZoom) { return false; }
var v=true; var i=0; var isAnd=this.isAnd;
array.forEach(this.conditions, function(condition) {
var r=condition.test(tags);
if (i==0) { v=r; }
else if (isAnd) { v=v && r; }
else { v = v || r; }
i++;
});
return v;
},
toString:function() {
return this.subject+" z"+this.minZoom+"-"+this.maxZoom+": "+this.conditions;
}
});
// ----------------------------------------------------------------------
// End of module
});

82
js/iD/styleparser/RuleChain.js Executable file
View File

@@ -0,0 +1,82 @@
// iD/styleparser/RuleChain.js
define(['dojo/_base/declare','iD/styleparser/Rule'], function(declare){
// ----------------------------------------------------------------------
// RuleChain base class
// In contrast to Halcyon, note that length() is a function, not a getter property
/** A descendant list of MapCSS selectors (Rules).
For example,
relation[type=route] way[highway=primary]
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
first Rule second Rule
|------------|---------|
|
one RuleChain
*/
declare("iD.styleparser.RuleChain", null, {
rules:[], // list of Rules
subpart: 'default', // subpart name, as in way[highway=primary]::centreline
constructor:function() {
this.rules=[];
},
// Functions to define the RuleChain
addRule:function(_subject) {
this.rules.push(new iD.styleparser.Rule(_subject));
},
addConditionToLast:function(_condition) {
this.rules[this.rules.length-1].addCondition(_condition);
},
addZoomToLast:function(z1,z2) {
this.rules[this.rules.length-1].minZoom=z1;
this.rules[this.rules.length-1].maxZoom=z2;
},
length:function() {
return this.rules.length;
},
setSubpart:function(_subpart) {
this.subpart = _subpart=='' ? 'default' : _subpart;
},
// Test a ruleChain
// - run a set of tests in the chain
// works backwards from at position "pos" in array, or -1 for the last
// separate tags object is required in case they've been dynamically retagged
// - if they fail, return false
// - if they succeed, and it's the last in the chain, return happily
// - if they succeed, and there's more in the chain, rerun this for each parent until success
test:function(pos, entity, tags, zoom) {
if (this.rules.length==0) { return false; }
if (pos==-1) { pos=this.rules.length-1; }
var r=this.rules[pos];
if (!r.test(entity, tags, zoom)) { return false; }
if (pos==0) { return true; }
var o=entity.parentObjects();
for (var i=0; i<o.length; i++) {
var p=o[i];
if (this.test(pos-1, p, p.getTagsHash(), zoom)) { return true; }
}
return false;
},
});
// ----------------------------------------------------------------------
// End of module
});

456
js/iD/styleparser/RuleSet.js Executable file
View File

@@ -0,0 +1,456 @@
// iD/styleparser/RuleSet.js
define(['dojo/_base/xhr','dojo/_base/lang','dojo/_base/declare','dojo/_base/array','iD/styleparser/Style','iD/styleparser/StyleChooser','iD/styleparser/Condition','iD/styleparser/StyleList'], function(xhr,lang,declare,array){
// ----------------------------------------------------------------------
// RuleSet base class
// needs to cope with nested CSS files
// evals not done
// doesn't do untagged nodes optimisation
declare("iD.styleparser.RuleSet", null, {
choosers: [], // list of StyleChoosers
callback: null,
constructor:function() {
this.choosers=[];
},
registerCallback:function(_callback) {
this.callback=_callback;
},
// Find the styles for a given entity
getStyles:function(entity, tags, zoom) {
var sl=new iD.styleparser.StyleList();
for (var i in this.choosers) {
this.choosers[i].updateStyles(entity, tags, sl, zoom);
}
return sl;
},
// Load from .mapcss file
loadFromCSS:function(url) {
xhr.get({ url: url, load: lang.hitch(this, "parseCSS") });
},
parseCSS:function(css) {
var previous=0; // what was the previous CSS word?
var sc=new iD.styleparser.StyleChooser(); // currently being assembled
this.choosers=[];
css=css.replace(/[\r\n]/g,""); // strip linebreaks because JavaScript doesn't have the /s modifier
var o={};
while (css.length>0) {
// CSS comment
if ((o=this.COMMENT.exec(css))) {
css=css.replace(this.COMMENT,'');
// Whitespace (probably only at beginning of file)
} else if ((o=this.WHITESPACE.exec(css))) {
css=css.replace(this.WHITESPACE,'');
// Class - .motorway, .builtup, :hover
} else if ((o=this.CLASS.exec(css))) {
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
css=css.replace(this.CLASS,'');
sc.currentChain().addConditionToLast(new iD.styleparser.Condition('set',o[1]));
previous=this.oCONDITION;
// Not class - !.motorway, !.builtup, !:hover
} else if ((o=this.NOT_CLASS.exec(css))) {
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
css=css.replace(this.NOT_CLASS,'');
sc.currentChain().addConditionToLast(new iD.styleparser.Condition('unset',o[1]));
previous=this.oCONDITION;
// Zoom
} else if ((o=this.ZOOM.exec(css))) {
if (previous!=this.oOBJECT && previous!=this.oCONDITION) { sc.currentChain().addRule(); }
css=css.replace(this.ZOOM,'');
var z=parseZoom(o[1]);
sc.currentChain().addZoomToLast(z[0],z[1]);
sc.zoomSpecific=true;
previous=this.oZOOM;
// Grouping - just a comma
} else if ((o=this.GROUP.exec(css))) {
css=css.replace(this.GROUP,'');
sc.newRuleChain();
previous=this.oGROUP;
// Condition - [highway=primary]
} else if ((o=this.CONDITION.exec(css))) {
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
if (previous!=this.oOBJECT && previous!=this.oZOOM && previous!=this.oCONDITION) { sc.currentChain().addRule(); }
css=css.replace(this.CONDITION,'');
sc.currentChain().addConditionToLast(this.parseCondition(o[1]));
previous=this.oCONDITION;
// Object - way, node, relation
} else if ((o=this.OBJECT.exec(css))) {
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
css=css.replace(this.OBJECT,'');
sc.currentChain().addRule(o[1]);
previous=this.oOBJECT;
// Subpart - ::centreline
} else if ((o=this.SUBPART.exec(css))) {
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
css=css.replace(this.SUBPART,'');
sc.currentChain().setSubpart(o[1]);
previous=this.oSUBPART;
// Declaration - {...}
} else if ((o=this.DECLARATION.exec(css))) {
css=css.replace(this.DECLARATION,'');
sc.addStyles(this.parseDeclaration(o[1]));
previous=this.oDECLARATION;
// Unknown pattern
} else if ((o=this.UNKNOWN.exec(css))) {
css=css.replace(this.UNKNOWN,'');
console.log("unknown: "+o[1]);
} else {
console.log("choked on "+css);
return;
}
}
if (previous==this.oDECLARATION) { this.saveChooser(sc); sc=new iD.styleparser.StyleChooser(); }
if (this.callback) { this.callback(); }
},
saveChooser:function(sc) {
this.choosers.push(sc);
},
parseDeclaration:function(s) {
var styles=[];
var t={};
var o={};
var k, v;
// Create styles
var ss=new iD.styleparser.ShapeStyle() ;
var ps=new iD.styleparser.PointStyle() ;
var ts=new iD.styleparser.TextStyle() ;
var hs=new iD.styleparser.ShieldStyle();
var xs=new iD.styleparser.InstructionStyle();
var r=s.split(';');
var isEval={};
for (var i in r) {
var a=r[i];
if ((o=this.ASSIGNMENT_EVAL.exec(a))) { k=o[1].replace(this.DASH,'_'); t[k]=o[2]; isEval[k]=true; }
else if ((o=this.ASSIGNMENT.exec(a))) { k=o[1].replace(this.DASH,'_'); t[k]=o[2]; }
else if ((o=this.SET_TAG_EVAL.exec(a))) { } // xs.addSetTag(o[1],this.saveEval(o[2]));
else if ((o=this.SET_TAG.exec(a))) { xs.addSetTag(o[1],o[2]); }
else if ((o=this.SET_TAG_TRUE.exec(a))) { xs.addSetTag(o[1],true); }
else if ((o=this.EXIT.exec(a))) { xs.setPropertyFromString('breaker',true); }
}
// Find sublayer
var sub=5;
if (t['z_index']) { sub=Number(t['z_index']); delete t['z_index']; }
ss.sublayer=ps.sublayer=ts.sublayer=hs.sublayer=sub;
xs.sublayer=10;
// Find "interactive" property - it's true unless explicitly set false
var inter=true;
if (t['interactive']) { inter=t['interactive'].match(this.FALSE) ? false : true; delete t['interactive']; }
ss.interactive=ps.interactive=ts.interactive=hs.interactive=xs.interactive=inter;
// Munge special values
// (we should stop doing this and do it in the style instead)
if (t['font_weight'] ) { t['font_bold' ] = t['font_weight' ].match(this.BOLD ) ? true : false; delete t['font_weight']; }
if (t['font_style'] ) { t['font_italic'] = t['font_style' ].match(this.ITALIC) ? true : false; delete t['font_style']; }
if (t['text_decoration']) { t['font_underline'] = t['text_decoration'].match(this.UNDERLINE) ? true : false; delete t['text_decoration']; }
if (t['text_position'] ) { t['text_center'] = t['text_position' ].match(this.CENTER) ? true : false; delete t['text_position']; }
if (t['text_transform']) {
if (t['text_transform'].match(this.CAPS)) { t['font_caps']=true; } else { t['font_caps']=false; }
delete t['text_transform'];
}
// Assign each property to the appropriate style
for (a in t) {
// Parse properties
// ** also do units, e.g. px/pt/m
if (a.match(this.COLOR)) { v = this.parseCSSColor(t[a]); }
else { v = t[a]; }
// Set in styles
if (ss.has(a)) { ss.setPropertyFromString(a,v,isEval[a]); }
else if (ps.has(a)) { ps.setPropertyFromString(a,v,isEval[a]); }
else if (ts.has(a)) { ts.setPropertyFromString(a,v,isEval[a]); }
else if (hs.has(a)) { hs.setPropertyFromString(a,v,isEval[a]); }
else { console.log(a+" not found"); }
}
// Add each style to list
if (ss.edited) { styles.push(ss); }
if (ps.edited) { styles.push(ps); }
if (ts.edited) { styles.push(ts); }
if (hs.edited) { styles.push(hs); }
if (xs.edited) { styles.push(xs); }
return styles;
},
parseZoom:function(s) {
var o={};
if ((o=this.ZOOM_MINMAX.exec(s))) { return [o[1],o[2]]; }
else if ((o=this.ZOOM_MIN.exec(s) )) { return [o[1],maxscale]; }
else if ((o=this.ZOOM_MAX.exec(s) )) { return [minscale,o[1]]; }
else if ((o=this.ZOOM_SINGLE.exec(s))) { return [o[1],o[1]]; }
return null;
},
parseCondition:function(s) {
var o={};
if ((o=this.CONDITION_TRUE.exec(s))) { return new iD.styleparser.Condition('true' ,o[1]); }
else if ((o=this.CONDITION_FALSE.exec(s))) { return new iD.styleparser.Condition('false',o[1]); }
else if ((o=this.CONDITION_SET.exec(s))) { return new iD.styleparser.Condition('set' ,o[1]); }
else if ((o=this.CONDITION_UNSET.exec(s))) { return new iD.styleparser.Condition('unset',o[1]); }
else if ((o=this.CONDITION_NE.exec(s))) { return new iD.styleparser.Condition('ne' ,o[1],o[2]); }
else if ((o=this.CONDITION_GT.exec(s))) { return new iD.styleparser.Condition('>' ,o[1],o[2]); }
else if ((o=this.CONDITION_GE.exec(s))) { return new iD.styleparser.Condition('>=' ,o[1],o[2]); }
else if ((o=this.CONDITION_LT.exec(s))) { return new iD.styleparser.Condition('<' ,o[1],o[2]); }
else if ((o=this.CONDITION_LE.exec(s))) { return new iD.styleparser.Condition('<=' ,o[1],o[2]); }
else if ((o=this.CONDITION_REGEX.exec(s))) { return new iD.styleparser.Condition('regex',o[1],o[2]); }
else if ((o=this.CONDITION_EQ.exec(s))) { return new iD.styleparser.Condition('eq' ,o[1],o[2]); }
return null;
},
parseCSSColor:function(colorStr) {
colorStr = colorStr.toLowerCase();
if (this.CSSCOLORS[colorStr]) {
return this.CSSCOLORS[colorStr];
} else {
var match = this.HEX.exec(colorStr);
if ( match ) {
if ( match[1].length == 3) {
// repeat digits. #abc => 0xaabbcc
return Number("0x"+match[1].charAt(0)+match[1].charAt(0)+
match[1].charAt(1)+match[1].charAt(1)+
match[1].charAt(2)+match[1].charAt(2));
} else if ( match[1].length == 6) {
return Number("0x"+match[1]);
} else {
return Number("0x000000"); //as good as any
}
}
}
return 0;
},
// Regular expression tests and other constants
WHITESPACE :/^\s+/,
COMMENT :/^\/\*.+?\*\/\s*/,
CLASS :/^([\.:]\w+)\s*/,
NOT_CLASS :/^!([\.:]\w+)\s*/,
ZOOM :/^\|\s*z([\d\-]+)\s*/i,
GROUP :/^,\s*/i,
CONDITION :/^\[(.+?)\]\s*/,
OBJECT :/^(\w+)\s*/,
DECLARATION :/^\{(.+?)\}\s*/,
SUBPART :/^::(\w+)\s*/,
UNKNOWN :/^(\S+)\s*/,
ZOOM_MINMAX :/^(\d+)\-(\d+)$/,
ZOOM_MIN :/^(\d+)\-$/,
ZOOM_MAX :/^\-(\d+)$/,
ZOOM_SINGLE :/^(\d+)$/,
CONDITION_TRUE :/^\s*([:\w]+)\s*=\s*yes\s*$/i,
CONDITION_FALSE :/^\s*([:\w]+)\s*=\s*no\s*$/i,
CONDITION_SET :/^\s*([:\w]+)\s*$/,
CONDITION_UNSET :/^\s*!([:\w]+)\s*$/,
CONDITION_EQ :/^\s*([:\w]+)\s*=\s*(.+)\s*$/,
CONDITION_NE :/^\s*([:\w]+)\s*!=\s*(.+)\s*$/,
CONDITION_GT :/^\s*([:\w]+)\s*>\s*(.+)\s*$/,
CONDITION_GE :/^\s*([:\w]+)\s*>=\s*(.+)\s*$/,
CONDITION_LT :/^\s*([:\w]+)\s*<\s*(.+)\s*$/,
CONDITION_LE :/^\s*([:\w]+)\s*<=\s*(.+)\s*$/,
CONDITION_REGEX :/^\s*([:\w]+)\s*=~\/\s*(.+)\/\s*$/,
ASSIGNMENT_EVAL :/^\s*(\S+)\s*\:\s*eval\s*\(\s*'(.+?)'\s*\)\s*$/i,
ASSIGNMENT :/^\s*(\S+)\s*\:\s*(.+?)\s*$/,
SET_TAG_EVAL :/^\s*set\s+(\S+)\s*=\s*eval\s*\(\s*'(.+?)'\s*\)\s*$/i,
SET_TAG :/^\s*set\s+(\S+)\s*=\s*(.+?)\s*$/i,
SET_TAG_TRUE :/^\s*set\s+(\S+)\s*$/i,
EXIT :/^\s*exit\s*$/i,
oZOOM: 2,
oGROUP: 3,
oCONDITION: 4,
oOBJECT: 5,
oDECLARATION: 6,
oSUBPART: 7,
DASH: /\-/g,
COLOR: /color$/,
BOLD: /^bold$/i,
ITALIC: /^italic|oblique$/i,
UNDERLINE: /^underline$/i,
CAPS: /^uppercase$/i,
CENTER: /^center$/i,
FALSE: /^(no|false|0)$/i,
HEX: /^#([0-9a-f]+)$/i,
CSSCOLORS: {
aliceblue:0xf0f8ff,
antiquewhite:0xfaebd7,
aqua:0x00ffff,
aquamarine:0x7fffd4,
azure:0xf0ffff,
beige:0xf5f5dc,
bisque:0xffe4c4,
black:0x000000,
blanchedalmond:0xffebcd,
blue:0x0000ff,
blueviolet:0x8a2be2,
brown:0xa52a2a,
burlywood:0xdeb887,
cadetblue:0x5f9ea0,
chartreuse:0x7fff00,
chocolate:0xd2691e,
coral:0xff7f50,
cornflowerblue:0x6495ed,
cornsilk:0xfff8dc,
crimson:0xdc143c,
cyan:0x00ffff,
darkblue:0x00008b,
darkcyan:0x008b8b,
darkgoldenrod:0xb8860b,
darkgray:0xa9a9a9,
darkgreen:0x006400,
darkkhaki:0xbdb76b,
darkmagenta:0x8b008b,
darkolivegreen:0x556b2f,
darkorange:0xff8c00,
darkorchid:0x9932cc,
darkred:0x8b0000,
darksalmon:0xe9967a,
darkseagreen:0x8fbc8f,
darkslateblue:0x483d8b,
darkslategray:0x2f4f4f,
darkturquoise:0x00ced1,
darkviolet:0x9400d3,
deeppink:0xff1493,
deepskyblue:0x00bfff,
dimgray:0x696969,
dodgerblue:0x1e90ff,
firebrick:0xb22222,
floralwhite:0xfffaf0,
forestgreen:0x228b22,
fuchsia:0xff00ff,
gainsboro:0xdcdcdc,
ghostwhite:0xf8f8ff,
gold:0xffd700,
goldenrod:0xdaa520,
gray:0x808080,
green:0x008000,
greenyellow:0xadff2f,
honeydew:0xf0fff0,
hotpink:0xff69b4,
indianred:0xcd5c5c,
indigo:0x4b0082,
ivory:0xfffff0,
khaki:0xf0e68c,
lavender:0xe6e6fa,
lavenderblush:0xfff0f5,
lawngreen:0x7cfc00,
lemonchiffon:0xfffacd,
lightblue:0xadd8e6,
lightcoral:0xf08080,
lightcyan:0xe0ffff,
lightgoldenrodyellow:0xfafad2,
lightgrey:0xd3d3d3,
lightgreen:0x90ee90,
lightpink:0xffb6c1,
lightsalmon:0xffa07a,
lightseagreen:0x20b2aa,
lightskyblue:0x87cefa,
lightslategray:0x778899,
lightsteelblue:0xb0c4de,
lightyellow:0xffffe0,
lime:0x00ff00,
limegreen:0x32cd32,
linen:0xfaf0e6,
magenta:0xff00ff,
maroon:0x800000,
mediumaquamarine:0x66cdaa,
mediumblue:0x0000cd,
mediumorchid:0xba55d3,
mediumpurple:0x9370d8,
mediumseagreen:0x3cb371,
mediumslateblue:0x7b68ee,
mediumspringgreen:0x00fa9a,
mediumturquoise:0x48d1cc,
mediumvioletred:0xc71585,
midnightblue:0x191970,
mintcream:0xf5fffa,
mistyrose:0xffe4e1,
moccasin:0xffe4b5,
navajowhite:0xffdead,
navy:0x000080,
oldlace:0xfdf5e6,
olive:0x808000,
olivedrab:0x6b8e23,
orange:0xffa500,
orangered:0xff4500,
orchid:0xda70d6,
palegoldenrod:0xeee8aa,
palegreen:0x98fb98,
paleturquoise:0xafeeee,
palevioletred:0xd87093,
papayawhip:0xffefd5,
peachpuff:0xffdab9,
peru:0xcd853f,
pink:0xffc0cb,
plum:0xdda0dd,
powderblue:0xb0e0e6,
purple:0x800080,
red:0xff0000,
rosybrown:0xbc8f8f,
royalblue:0x4169e1,
saddlebrown:0x8b4513,
salmon:0xfa8072,
sandybrown:0xf4a460,
seagreen:0x2e8b57,
seashell:0xfff5ee,
sienna:0xa0522d,
silver:0xc0c0c0,
skyblue:0x87ceeb,
slateblue:0x6a5acd,
slategray:0x708090,
snow:0xfffafa,
springgreen:0x00ff7f,
steelblue:0x4682b4,
tan:0xd2b48c,
teal:0x008080,
thistle:0xd8bfd8,
tomato:0xff6347,
turquoise:0x40e0d0,
violet:0xee82ee,
wheat:0xf5deb3,
white:0xffffff,
whitesmoke:0xf5f5f5,
yellow:0xffff00,
yellowgreen:0x9acd32 },
});
// ----------------------------------------------------------------------
// End of module
});

243
js/iD/styleparser/Style.js Executable file
View File

@@ -0,0 +1,243 @@
// iD/styleparser/Style.js
// Entity classes for iD
define(['dojo/_base/declare','dojo/_base/array'], function(declare,array){
// ----------------------------------------------------------------------
// Style base class
// don't use deepCopy, use lang.clone instead
// evals not done yet
// fillStyler not done for text yet
declare("iD.styleparser.Style", null, {
merged: false,
edited: false,
sublayer: 5,
interactive: true,
properties: [],
styleType: 'Style',
evals: null,
constructor: function(){
this.evals={};
},
drawn: function(){
return false;
},
has: function(k){
return this.properties.indexOf(k)>-1;
},
mergeWith: function(additional) {
for (var prop in this.properties) {
if (additional[prop]) {
this[prop]=additional[prop];
}
}
this.merged=true;
},
setPropertyFromString: function(k,v,isEval) {
this.edited=true;
if (isEval) { this.evals[k]=v; return; }
if (typeof(this[k])=='boolean') {
v=Boolean(v);
} else if (typeof(this[k])=='number') {
v=Number(v);
} else if (this[k] && this[k].constructor==Array) {
v=v.split(',').map(function(a) { return Number(a); });
}
this[k]=v;
return true;
},
runEvals: function(tags) {
for (var k in this.evals) {
this.setPropertyFromString(k,eval("with (tags) {"+this.evals[k]+"}"),false);
}
},
dojoColor: function(rgb,a) {
var b=rgb % 256;
var g=(rgb-b) % 65536;
var r=(rgb-b-g) % 16777216;
return new dojo.Color([r/65536,g/256,b,a]);
},
toString: function() {
var str='';
for (var k in this.properties) {
if (this.hasOwnProperty(k)) { str+=k+"="+this[k]+"; "; }
}
return str;
},
});
// ----------------------------------------------------------------------
// InstructionStyle class
declare("iD.styleparser.InstructionStyle", [iD.styleparser.Style], {
set_tags:null,
breaker:false,
styleType: 'InstructionStyle',
addSetTag:function(k,v) {
this.edited=true;
if (!this.set_tags) this.set_tags={};
this.set_tags[k]=v;
},
});
// ----------------------------------------------------------------------
// PointStyle class
declare("iD.styleparser.PointStyle", [iD.styleparser.Style], {
properties: ['icon_image','icon_width','icon_height','rotation'],
icon_image: null,
icon_width: 0,
icon_height: NaN,
rotation: NaN,
styleType: 'PointStyle',
drawn:function() {
return (this.icon_image!=null);
},
maxwidth:function() {
return this.evals['icon_width'] ? 0 : this.icon_width;
},
});
// ----------------------------------------------------------------------
// ShapeStyle class
declare("iD.styleparser.ShapeStyle", [iD.styleparser.Style], {
properties: ['width','color','opacity','dashes','linecap','linejoin','line_style',
'fill_image','fill_color','fill_opacity','casing_width','casing_color','casing_opacity','casing_dashes','layer'],
width:0, color:NaN, opacity:NaN, dashes:[],
linecap:null, linejoin:null, line_style:null,
fill_image:null, fill_color:NaN, fill_opacity:NaN,
casing_width:NaN, casing_color:NaN, casing_opacity:NaN, casing_dashes:[],
layer:NaN, // optional layer override (usually set by OSM tag)
styleType: 'ShapeStyle',
drawn:function() {
return (this.fill_image || !isNaN(this.fill_color) || this.width || this.casing_width);
},
maxwidth:function() {
// If width is set by an eval, then we can't use it to calculate maxwidth, or it'll just grow on each invocation...
if (this.evals['width'] || this.evals['casing_width']) { return 0; }
return (this.width + (this.casing_width ? this.casing_width*2 : 0));
},
strokeStyler:function() {
var cap,join;
switch (this.linecap ) { case 'round': cap ='round'; break; case 'square': cap='square'; break; default: cap ='butt' ; break; }
switch (this.linejoin) { case 'bevel': join='bevel'; break; case 'miter' : join=4 ; break; default: join='round'; break; }
return {
color: this.dojoColor(this.color ? this.color : 0, this.opacity ? this.opacity : 1),
style: 'Solid', // needs to parse dashes
width: this.width,
cap: cap,
join: join
};
},
shapeStrokeStyler:function() {
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
};
},
shapeFillStyler:function() {
if (isNaN(this.color)) { return null; }
return this.dojoColor(this.color, this.opacity ? this.opacity : 1);
},
fillStyler:function() {
return this.dojoColor(this.fill_color, this.fill_opacity ? this.fill_opacity : 1);
},
casingStyler:function() {
var cap,join;
switch (this.linecap ) { case 'round': cap ='round'; break; case 'square': cap='square'; break; default: cap ='butt' ; break; }
switch (this.linejoin) { case 'bevel': join='bevel'; break; case 'miter' : join=4 ; break; default: join='round'; break; }
return {
color: this.dojoColor(this.casing_color ? this.casing_color : 0, this.casing_opacity ? this.casing_opacity : 1),
width: this.width+this.casing_width*2,
style: 'Solid',
cap: cap,
join: join
};
},
});
// ----------------------------------------------------------------------
// TextStyle class
declare("iD.styleparser.TextStyle", [iD.styleparser.Style], {
properties: ['font_family','font_bold','font_italic','font_caps','font_underline','font_size',
'text_color','text_offset','max_width',
'text','text_halo_color','text_halo_radius','text_center',
'letter_spacing'],
font_family: null,
font_bold: false,
font_italic: false,
font_underline: false,
font_caps: false,
font_size: NaN,
text_color: NaN,
text_offset: NaN,
max_width: NaN,
text: null,
text_halo_color: NaN,
text_halo_radius: 0,
text_center: true,
letter_spacing: 0,
styleType: 'TextStyle',
drawn: function() {
return (this.text!=null);
},
fontStyler:function() {
return {
family: this.font_family ? this.font_family : 'Arial',
size: this.font_size ? this.font_size*2 : '10px' ,
weight: this.font_bold ? 'bold' : 'normal',
style: this.font_italic ? 'italic' : 'normal'
};
},
textStyler:function(_text) {
return {
decoration: this.font_underline ? 'underline' : 'none',
align: 'middle',
text: _text,
};
},
fillStyler:function() {
// not implemented yet
return this.dojoColor(0,1);
},
// getTextFormat, getHaloFilter, writeNameLabel
});
// ----------------------------------------------------------------------
// ShieldStyle class
declare("iD.styleparser.ShieldStyle", [iD.styleparser.Style], {
properties: ['shield_image','shield_width','shield_height'],
shield_image: null,
shield_width: NaN,
shield_height: NaN,
styleType: 'ShieldStyle',
drawn:function() {
return (shield_image!=null);
},
});
// ----------------------------------------------------------------------
// End of module
});

View File

@@ -0,0 +1,86 @@
// iD/styleparser/StyleChooser.js
define(['dojo/_base/declare','dojo/_base/lang','iD/styleparser/RuleChain'], function(declare,lang){
declare("iD.styleparser.StyleChooser", null, {
// UpdateStyles doesn't support image-widths yet
// or setting maxwidth/_width
ruleChains:[], // array of RuleChains (each one an array of Rules)
styles:[], // array of ShapeStyle/ShieldStyle/TextStyle/PointStyle
zoomSpecific:false, // are any of the rules zoom-specific?
rcpos:0,
stylepos:0,
constructor:function() {
this.ruleChains=[new iD.styleparser.RuleChain()];
this.styles=[];
},
currentChain:function() {
return this.ruleChains[this.ruleChains.length-1];
},
newRuleChain:function() {
// starts a new ruleChain in this.ruleChains
if (this.ruleChains[this.ruleChains.length-1].length()>0) {
this.ruleChains.push(new iD.styleparser.RuleChain());
}
},
addStyles:function(a) {
this.styles=this.styles.concat(a);
},
updateStyles:function(entity, tags, sl, zoom) {
if (this.zoomSpecific) { sl.validAt=zoom; }
// Are any of the ruleChains fulfilled?
var w;
for (var i in this.ruleChains) {
var c=this.ruleChains[i];
if (c.test(-1, entity, tags, zoom)) {
sl.addSubpart(c.subpart);
// Update StyleList
for (var j in this.styles) {
var r=this.styles[j];
var a;
switch (r.styleType) {
case 'ShapeStyle' : sl.maxwidth=Math.max(sl.maxwidth,r.maxwidth());
a=sl.shapeStyles; break;
case 'ShieldStyle': a=sl.shieldStyles; break;
case 'TextStyle' : a=sl.textStyles; break;
case 'PointStyle' : sl.maxwidth=Math.max(sl.maxwidth,r.maxwidth());
a=sl.pointStyles; break;
case 'InstructionStyle':
if (InstructionStyle(r).breaker) { return; }
for (var k in InstructionStyle(r).set_tags) { tags[k]=InstructionStyle(r).set_tags[k]; }
break;
}
if (r.drawn) { tags[':drawn']='yes'; }
tags['_width']=sl.maxwidth;
r.runEvals(tags);
if (a[c.subpart]) {
// If there's already a style on this sublayer, then merge them
// (making a deep copy if necessary to avoid altering the root style)
if (!a[c.subpart].merged) { a[c.subpart]=lang.clone(a[c.subpart]); }
a[c.subpart].mergeWith(r);
} else {
// Otherwise, just assign it
a[c.subpart]=r;
}
}
}
}
},
});
// ----------------------------------------------------------------------
// End of module
});

84
js/iD/styleparser/StyleList.js Executable file
View File

@@ -0,0 +1,84 @@
// iD/styleparser/StyleList.js
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// StyleList class
// Not tested yet!
// A StyleList object is the full list of all styles applied to
// a drawn entity (i.e. node/way).
// Each array element applies to that sublayer (z-index). If there
// is no element, nothing is drawn on that sublayer.
// StyleLists are created by StyleChooser.getStyles.
declare("iD.styleparser.StyleList", null, {
shapeStyles:{},
textStyles:{},
pointStyles:{},
shieldStyles:{},
maxwidth:0,
subparts:[], // List of subparts used in this StyleList
validAt:-1, // Zoom level this is valid at (or -1 at all levels - saves recomputing)
constructor:function() {
this.shapeStyles={};
this.textStyles={};
this.pointStyles={};
this.shieldStyles={};
this.subparts=[];
},
// Does this StyleList contain any styles?
hasStyles:function() {
return ( this.hasShapeStyles() || this.hasTextStyles() || this.hasPointStyles() || this.hasShieldStyles() );
},
// Does this StyleList contain any styles with a fill?
hasFills:function() {
for (var s in this.shapeStyles) {
if (!isNaN(this.shapeStyles(s).fill_color) || this.shapeStyles(s).fill_image) return true;
}
return false;
},
// Does this StyleList manually force an OSM layer?
layerOverride:function() {
for (var s in this.shapeStyles) {
if (!isNaN(this.shapeStyles[s].layer)) return this.shapeStyles[s].layer;
}
return NaN;
},
// Record that a subpart is used in this StyleList.
addSubpart:function(s) {
if (this.subparts.indexOf(s)==-1) { this.subparts.push(s); }
},
// Is this StyleList valid at a given zoom?
isValidAt:function(zoom) {
return (this.validAt==-1 || this.validAt==zoom);
},
// Summarise StyleList as String - for debugging
toString:function() {
var str='';
var k;
for (k in this.shapeStyles ) { str+="- SS "+k+"="+this.shapeStyles[k]+"\n"; }
for (k in this.textStyles ) { str+="- TS "+k+"="+this.textStyles[k]+"\n"; }
for (k in this.pointStyles ) { str+="- PS "+k+"="+this.pointStyles[k]+"\n"; }
for (k in this.shieldStyles) { str+="- sS "+k+"="+this.shieldStyles[k]+"\n"; }
return str;
},
hasShapeStyles:function() { for (var a in shapeStyles ) { return true; }; return false; },
hasTextStyles:function() { for (var a in textStyles ) { return true; }; return false; },
hasPointStyles:function() { for (var a in pointStyles ) { return true; }; return false; },
hasShieldStyles:function() { for (var a in shieldStyles) { return true; }; return false; },
});
// ----------------------------------------------------------------------
// End of module
});

97
js/iD/ui/DragAndDrop.js Normal file
View File

@@ -0,0 +1,97 @@
// iD/ui/DragAndDrop.js
/*
Singleton-like class for POI drag and drop.
Could potentially be a ControllerState.
*/
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/xhr','dojo/dom','dojo/dom-geometry','dojo/dnd/Target'], function(declare,lang,xhr,dom,domGeom){
// ----------------------------------------------------------------------
// DragAndDrop class
declare("iD.ui.DragAndDrop", null, {
mapdiv:null,
map:null,
divname:"",
grid:null,
dragmove:null,
dragevent:null,
ICONPATH: 'icons/',
ITEMSPERROW: 5,
constructor:function(_divname,_map,_gridname) {
this.divname=_divname;
dom.byId(_divname).ondragover = lang.hitch(this,this.update);
dom.byId(_divname).ondrop = function(e) { e.preventDefault(); }; // required by Firefox
this.map=_map;
this.grid=dom.byId(_gridname);
// Load drag and drop config file
dojo.xhrGet({
url: "draganddrop.json",
handleAs: "json",
load: lang.hitch(this, function(obj) { this.drawGrid(obj); } ),
error: function(err) { console.log("couldn't load"); }
});
},
drawGrid:function(obj) {
var row;
for (var i=0; i<obj.length; i++) {
var item=obj[i];
if (!row || row.length==this.ITEMSPERROW) {
row=document.createElement('tr');
this.grid.appendChild(row);
}
var cell=document.createElement('td');
var img=document.createElement('img');
img.setAttribute('src',this.ICONPATH+item.icon)
img.setAttribute('alt',item.tags);
img.setAttribute('draggable',true);
img.ondragend=lang.hitch(this,this.end);
img.style.float='left';
cell.appendChild(img);
cell.appendChild(document.createTextNode(item.name));
row.appendChild(cell);
}
},
update:function(event) {
this.dragevent=event;
event.preventDefault();
},
end:function(event) {
var lon=this.map.coord2lon(this.map.mouseX(this.dragevent));
var lat=this.map.coord2lat(this.map.mouseY(this.dragevent));
var tags=this.parseKeyValues(event.target.getAttribute('alt'));
var action=new iD.actions.CreatePOIAction(this.map.conn,tags,lat,lon);
this.map.controller.undoStack.addAction(action);
var node=action.getNode();
this.map.createUI(node);
dijit.byId('addPOI').closeDropDown();
this.map.controller.setState(new iD.controller.edit.SelectedPOINode(node));
},
parseKeyValues:function(string) {
var pairs=string.split(';');
var tags={};
for (var i in pairs) {
var kv=pairs[i].split('=');
tags[kv[0]]=kv[1];
}
return tags;
},
});
// ----------------------------------------------------------------------
// End of module
});

79
js/iD/ui/StepPane.js Normal file
View File

@@ -0,0 +1,79 @@
// iD/ui/StepPane.js
/*
Step-by-step help pane.
*/
define(['dojo/_base/declare','dojo/_base/lang'], function(declare,lang){
// ----------------------------------------------------------------------
// StepPane class
// ******
// This is a bit messy at present - it shouldn't take stepsname or similar, it should just
// create the pane programmatically.
// ******
// We should also be able to set the title of the pane.
declare("iD.ui.StepPane", null, {
divname:null,
stepsname:null, // we probably don't want to have this
currentStep:0,
constructor:function(_divname,_stepsdivname) {
this.divname=_divname;
this.stepsname=_stepsdivname;
},
// Getters for the <div> containing the steps, and its individual nodes
stepsDiv:function() { return document.getElementById(this.stepsname); },
stepsNodes:function() { return this.stepsDiv().childNodes; },
// Add/remove steps
addStep:function(text) {
this.stepsDiv().appendChild(document.createElement('li')).innerHTML=text;
},
setStep:function(pos,text) {
if (this.stepsNodes().length<pos) { addStep(text); }
else { this.stepsNodes()[pos].innerHTML=text; }
},
setSteps:function(steps) {
this.clear();
for (var i=0; i<steps.length; i++) {
this.addStep(steps[i]);
}
return this;
},
clear:function() {
for (var i=this.stepsNodes().length-1; i>=1; i--) {
this.stepsDiv().removeChild(this.stepsNodes()[i]);
}
},
// Change the highlighted step
highlight:function(step) {
this.show();
this.currentStep=step;
for (var i=1; i<this.stepsNodes().length; i++) {
this.stepsNodes()[i].style.color = i==step ? 'black' : 'lightgray';
}
},
// Show/hide window
show:function() {
dijit.byId(this.divname).show();
},
hide:function() {
dijit.byId(this.divname).hide();
},
});
// ----------------------------------------------------------------------
// End of module
});

16
js/lib/jshashtable.js Executable file
View File

@@ -0,0 +1,16 @@
/**
* Copyright 2010 Tim Down.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var Hashtable=(function(){var p="function";var n=(typeof Array.prototype.splice==p)?function(s,r){s.splice(r,1)}:function(u,t){var s,v,r;if(t===u.length-1){u.length=t}else{s=u.slice(t+1);u.length=t;for(v=0,r=s.length;v<r;++v){u[t+v]=s[v]}}};function a(t){var r;if(typeof t=="string"){return t}else{if(typeof t.hashCode==p){r=t.hashCode();return(typeof r=="string")?r:a(r)}else{if(typeof t.toString==p){return t.toString()}else{try{return String(t)}catch(s){return Object.prototype.toString.call(t)}}}}}function g(r,s){return r.equals(s)}function e(r,s){return(typeof s.equals==p)?s.equals(r):(r===s)}function c(r){return function(s){if(s===null){throw new Error("null is not a valid "+r)}else{if(typeof s=="undefined"){throw new Error(r+" must not be undefined")}}}}var q=c("key"),l=c("value");function d(u,s,t,r){this[0]=u;this.entries=[];this.addEntry(s,t);if(r!==null){this.getEqualityFunction=function(){return r}}}var h=0,j=1,f=2;function o(r){return function(t){var s=this.entries.length,v,u=this.getEqualityFunction(t);while(s--){v=this.entries[s];if(u(t,v[0])){switch(r){case h:return true;case j:return v;case f:return[s,v[1]]}}}return false}}function k(r){return function(u){var v=u.length;for(var t=0,s=this.entries.length;t<s;++t){u[v+t]=this.entries[t][r]}}}d.prototype={getEqualityFunction:function(r){return(typeof r.equals==p)?g:e},getEntryForKey:o(j),getEntryAndIndexForKey:o(f),removeEntryForKey:function(s){var r=this.getEntryAndIndexForKey(s);if(r){n(this.entries,r[0]);return r[1]}return null},addEntry:function(r,s){this.entries[this.entries.length]=[r,s]},keys:k(0),values:k(1),getEntries:function(s){var u=s.length;for(var t=0,r=this.entries.length;t<r;++t){s[u+t]=this.entries[t].slice(0)}},containsKey:o(h),containsValue:function(s){var r=this.entries.length;while(r--){if(s===this.entries[r][1]){return true}}return false}};function m(s,t){var r=s.length,u;while(r--){u=s[r];if(t===u[0]){return r}}return null}function i(r,s){var t=r[s];return(t&&(t instanceof d))?t:null}function b(t,r){var w=this;var v=[];var u={};var x=(typeof t==p)?t:a;var s=(typeof r==p)?r:null;this.put=function(B,C){q(B);l(C);var D=x(B),E,A,z=null;E=i(u,D);if(E){A=E.getEntryForKey(B);if(A){z=A[1];A[1]=C}else{E.addEntry(B,C)}}else{E=new d(D,B,C,s);v[v.length]=E;u[D]=E}return z};this.get=function(A){q(A);var B=x(A);var C=i(u,B);if(C){var z=C.getEntryForKey(A);if(z){return z[1]}}return null};this.containsKey=function(A){q(A);var z=x(A);var B=i(u,z);return B?B.containsKey(A):false};this.containsValue=function(A){l(A);var z=v.length;while(z--){if(v[z].containsValue(A)){return true}}return false};this.clear=function(){v.length=0;u={}};this.isEmpty=function(){return !v.length};var y=function(z){return function(){var A=[],B=v.length;while(B--){v[B][z](A)}return A}};this.keys=y("keys");this.values=y("values");this.entries=y("getEntries");this.remove=function(B){q(B);var C=x(B),z,A=null;var D=i(u,C);if(D){A=D.removeEntryForKey(B);if(A!==null){if(!D.entries.length){z=m(v,C);n(v,z);delete u[C]}}}return A};this.size=function(){var A=0,z=v.length;while(z--){A+=v[z].entries.length}return A};this.each=function(C){var z=w.entries(),A=z.length,B;while(A--){B=z[A];C(B[0],B[1])}};this.putAll=function(H,C){var B=H.entries();var E,F,D,z,A=B.length;var G=(typeof C==p);while(A--){E=B[A];F=E[0];D=E[1];if(G&&(z=w.get(F))){D=C(F,z,D)}w.put(F,D)}};this.clone=function(){var z=new b(t,r);z.putAll(w);return z}}return b})();

151
potlatch.css Executable file
View File

@@ -0,0 +1,151 @@
/*
Stylesheet that mimicks, to a certain extent, potlatch 1.x
Andy Allan, November 2009
Based heavily on:
MapCSS demonstration stylesheet
Richard Fairhurst, October 2009
*/
/* A set of fairly standard rules.
We use z-index to make sure high-priority roads appear above minor ones.
The default z-index is 5. If an object matches multiple rules with the same
z-index then the rules are "merged" (but individual properties become one or the other) */
way[highway=motorway],way[highway=motorway_link],
way[highway=trunk],way[highway=trunk_link],
way[highway=primary],way[highway=primary_link],
way[highway=secondary],way[highway=secondary_link],
way[highway=tertiary],way[highway=tertiary_link],
way[highway=unclassified],
way[highway=residential] { text: name; text-color: black; font-size: 7; text-position: line;}
way[highway=motorway],way[highway=motorway_link] { z-index: 9; color: #809BC0; width: 7; casing-color: black; casing-width: 1; }
way[highway=trunk],way[highway=trunk_link] { z-index: 9; color: #7FC97F; width: 7; casing-color: black; casing-width: 1; }
way[highway=primary],way[highway=primary_link] { z-index: 8; color: #E46D71; width: 7; casing-color: black; casing-width: 1; }
way[highway=secondary],way[highway=secondary_link] { z-index: 7; color: #FDBF6F; width: 7; casing-width: 1; }
way[highway=tertiary],way[highway=unclassified] { z-index: 6; color: #FEFECB; width: 5; casing-width: 1; }
way[highway=residential] { z-index: 5; color: #E8E8E8; width: 5; casing-color: gray; casing-width: 1; }
way[highway=service] { color: white; width: 3; casing-width: 1; }
/* Pedestrian precincts need to be treated carefully. Only closed-loops with an explicit
area=yes tag should be filled. The below doesn't yet work as intended. */
way[highway=pedestrian] !:area { color: #ddddee; width: 5; casing-color: #555555; casing-width: 1; }
way[highway=pedestrian] :area { color: #555555; width: 1; fill-color: #ddddee; fill-opacity: 0.8; }
way[highway=steps] { color: #FF6644; width: 2; dashes: 4, 2; }
way[highway=footway] { color: #FF6644; width: 2; dashes: 6, 3; }
way[highway=bridleway] { z-index:9; color: #996644; width: 2; dashes: 4, 2, 2, 2; }
way[highway=track] { color: #996644; width: 2; dashes: 4, 2; }
way[highway=path] { color: lightgreen; width: 2; dashes: 2, 2; }
way[waterway=river], way[waterway=canal] { color: blue; width: 2; text:name; text-color:blue; font-size:9; text-position: offset; text-offset: 7;}
way[barrier] {color: #000000; width: 1}
/* Fills can be solid colour or bitmap images */
way[natural] :area { color: #ADD6A5; width: 1; fill-color: #ADD6A5; fill-opacity: 0.2; }
way[landuse] :area { color: #444444; width: 2; fill-color: #444444; fill-opacity: 0.3; }
way[amenity],way[shop] :area { color: #ADCEB5; width: 1; fill-color: #ADCEB5; fill-opacity: 0.2; }
way[leisure],way[sport] :area { color: #8CD6B5; width: 1; fill-color: #8CD6B5; fill-opacity: 0.2; }
way[tourism] :area { color: #F7CECE; width: 1; fill-color: #F7CECE; fill-opacity: 0.2; }
way[historic],way[ruins] :area { color: #F7F7DE; width: 1; fill-color: #F7F7DE; fill-opacity: 0.2; }
way[military] :area { color: #D6D6D6; width: 1; fill-color: #D6D6D6; fill-opacity: 0.2; }
way[building] :area { color: #ff6ec7; width: 1; fill-color: #ff6ec7; fill-opacity: 0.2; }
way[natural=water],
way[waterway] :area { color: blue; width: 2; fill-color: blue; fill-opacity: 0.2; }
way[landuse=forest],way[natural=wood] :area { color: green; width: 2; fill-color: green; fill-opacity: 0.2; }
way[leisure=pitch],way[leisure=park] { color: #44ff44; width: 1; fill-color: #44ff44; fill-opacity: 0.2; }
way[amenity=parking] :area { color: gray; width: 1; fill-color: gray; fill-opacity: 0.2; }
way[public_transport=pay_scale_area] :area { color: gray; width: 1; fill-color: gray; fill-opacity: 0.1; }
/* Addressing. Nodes with addresses *and* match POIs should have a poi icon, so we put addressing first */
node[addr:housenumber],
node[addr:housename] { icon-image: circle; icon-width: 4; color: #B0E0E6; casing-color:blue; casing-width: 1; }
way[addr:interpolation] { color: #B0E0E6; width: 3; dashes: 3,3;}
/* POIs, too, can have bitmap icons - they can even be transparent */
node[amenity=pub] { icon-image: icons/pub.png; text-offset: 15; font-family: DejaVu; text: name; font-size: 9; }
node[place] { icon-image: icons/place.png; text-offset: 17; font-family: DejaVu; text: name; font-size: 9; font-weight: bold; text-decoration: underline; }
node[railway=station] { icon-image: icons/station.png; text-offset: 13; font-family: DejaVu; text: name; font-size: 9; font-weight: bold; }
node[aeroway=aerodrome] { icon-image: icons/airport.png; text-offset: 13; font-family: DejaVu; text: name; font-size: 10; }
node[amenity=atm] { icon-image: icons/atm.png; }
node[amenity=bank] { icon-image: icons/bank.png; text-offset: 15; text: name; }
node[highway=bus_stop] { icon-image: icons/bus_stop.png; }
node[amenity=cafe] { icon-image: icons/cafe.png; text-offset: 15; text: name; icon-width: 16; }
node[shop=convenience] { icon-image: icons/convenience.png; text-offset:15; text:name; }
node[shop=supermarket] { icon-image: icons/supermarket.png; text-offset:15; text:name; }
node[amenity=fast_food] { icon-image: icons/fast_food.png; text-offset:15; text: name; }
node[amenity=fire_station] { icon-image: icons/fire_station.png; }
node[amenity=hospital] { icon-image: icons/hospital.png; }
node[tourism=hotel] { icon-image: icons/hotel.png; }
node[amenity=parking] { icon-image: icons/parking.png; }
node[amenity=bicycle_parking] { icon-image: icons/parking_cycle.png; text-offset: 15; text: capacity; }
node[amenity=pharmacy] { icon-image: icons/pharmacy.png; }
node[amenity=pharmacy][dispensing=yes] { icon-image: icons/pharmacy_dispensing.png; }
node[amenity=police] { icon-image: icons/police.png; }
node[amenity=post_box] { icon-image: icons/post_box.png; }
node[amenity=recycling] { icon-image: icons/recycling.png; }
node[amenity=restaurant] { icon-image: icons/restaurant.png; icon-width: 16; }
node[amenity=school] { icon-image: icons/school.png; icon-width: 16; }
node[amenity=taxi] { icon-image: icons/taxi.png; }
node[amenity=telephone] { icon-image: icons/telephone.png; }
way node[barrier=gate], way node[highway=gate] { icon-image: icons/gate.png; }
/* We can stack styles at different z-index (depth) */
way[railway=rail]
{ z-index: 6; color: black; width: 5; }
{ z-index: 7; color: white; width: 3; dashes: 12,12; }
way[railway=subway]
{ z-index: 6; color: #444444; width: 5; }
{ z-index: 7; color: white; width: 3; dashes: 8,8; }
/* Bridge */
way[bridge=yes]
{ z-index: 4; color: white; width: eval('_width+3'); }
{ z-index: 3; color: black; width: eval('_width+6'); }
/* Tunnel */
way[tunnel=yes]
{ z-index: 4; color: white; width: eval('_width+2'); }
{ z-index: 3; color: black; width: eval('_width+6'); dashes: 4,4; }
/* Oneway */
way[oneway=yes] { z-index: 10; color: #444444; width: 3; dashes: 15,25; line-style: arrows; }
/* Change the road colour based on dynamically set "highlighted" tag (see earlier) */
way .highlighted { color: pink; }
/* Interactive editors may choose different behaviour when a user mouses-over or selects
an object. Potlatch 2 supports these but the stand-alone Halcyon viewer does not */
way !:drawn { z-index:10; width: 1; color: black; }
way::highlight :hover { z-index: 2; width: eval('_width+10'); color: #ffff99; }
way::highlight :selected { z-index: 2; width: eval('_width+10'); color: yellow; opacity: 0.7;}
node :selectedway { z-index: 9; icon-image: square; icon-width: 8; color: red; }
node::junction :junction :selectedway { z-index: 8; icon-image: square; icon-width: 11; casing-color: black; casing-width: 1; }
node !:drawn :poi { z-index: 2; icon-image: circle; icon-width: 4; color: green; casing-color: black; casing-width: 1; }
node :hoverway { z-index: 9; icon-image: square; icon-width: 7; color: blue; }
node::highlight :selected { z-index: 1; icon-image: square; icon-width: eval('_width+10'); color: yellow; }
/* Descendant selectors provide an easy way to style relations: this example means "any way
which is part of a relation whose type=route". */
relation[type=route] way { z-index: 1; width: 17; color: blue; opacity: 0.3; }
relation[type=route][route=bicycle][network=ncn] way { z-index: 1; width: 12; color: red; opacity: 0.3; }
relation[type=route][route=bicycle][network=rcn] way { z-index: 1; width: 12; color: cyan; opacity: 0.3; }
relation[type=route][route=bicycle][network=lcn] way { z-index: 1; width: 12; color: blue; opacity: 0.3; }
relation[type=route][route=foot] way { z-index: 1; width: 10; color: #80ff80; opacity: 0.6; }