.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
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
+});
diff --git a/js/iD/renderer/NodeUI.js b/js/iD/renderer/NodeUI.js
new file mode 100755
index 000000000..507b7cef8
--- /dev/null
+++ b/js/iD/renderer/NodeUI.js
@@ -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; i1) { sc.push('junction'); }
+ this.map.createUI(node,sc);
+ }
+ },
+
+ entityMouseEvent:function(event) {
+ this.inherited(arguments);
+ },
+});
+
+// ----------------------------------------------------------------------
+// End of module
+});
diff --git a/js/iD/styleparser/Condition.js b/js/iD/styleparser/Condition.js
new file mode 100755
index 000000000..c87b4cf82
--- /dev/null
+++ b/js/iD/styleparser/Condition.js
@@ -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
+});
\ No newline at end of file
diff --git a/js/iD/styleparser/Rule.js b/js/iD/styleparser/Rule.js
new file mode 100755
index 000000000..f3413144e
--- /dev/null
+++ b/js/iD/styleparser/Rule.js
@@ -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 (zoomthis.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
+});
diff --git a/js/iD/styleparser/RuleChain.js b/js/iD/styleparser/RuleChain.js
new file mode 100755
index 000000000..f83a0f4b0
--- /dev/null
+++ b/js/iD/styleparser/RuleChain.js
@@ -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; i0) {
+
+ // 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
+});
diff --git a/js/iD/styleparser/Style.js b/js/iD/styleparser/Style.js
new file mode 100755
index 000000000..fd763048f
--- /dev/null
+++ b/js/iD/styleparser/Style.js
@@ -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
+});
diff --git a/js/iD/styleparser/StyleChooser.js b/js/iD/styleparser/StyleChooser.js
new file mode 100755
index 000000000..82fb1f306
--- /dev/null
+++ b/js/iD/styleparser/StyleChooser.js
@@ -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
+});
diff --git a/js/iD/styleparser/StyleList.js b/js/iD/styleparser/StyleList.js
new file mode 100755
index 000000000..a85c4a19e
--- /dev/null
+++ b/js/iD/styleparser/StyleList.js
@@ -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
+});
diff --git a/js/iD/ui/DragAndDrop.js b/js/iD/ui/DragAndDrop.js
new file mode 100644
index 000000000..2c20cb240
--- /dev/null
+++ b/js/iD/ui/DragAndDrop.js
@@ -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 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=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