Start traversing the graph, other changes

Adds GeoJSON viewing to features, unifies the way that features refer
to their children.
This commit is contained in:
Tom MacWright
2012-11-01 10:55:33 -04:00
parent beeef7a561
commit 75d9331233
14 changed files with 107 additions and 189 deletions

View File

@@ -11,17 +11,35 @@ To be clear, this data model is something like
\- ways -> nodes
\- nodes
## Actions
Actions are operations on OSM data like adding nodes, moving ways,
and so on. They are initiated by controller states, like
`iD.controller.ControllerState` initiates a `CreatePOIAction` and
adds it to the undo stack.
## Performance
## Entities
Main performance concerns of iD:
`iD.Entity` is the door from pure objects like `iD.Node` into a hierarchy
of objects - it provides handling of parents, children, and so on.
### Panning & zooming performance of the map
SVG redraws are costly, especially when they require all features to
be reprojected.
Approaches:
* Using CSS transforms for intermediate map states, and then redrawing when
map movement stops
* "In-between" projecting features to make reprojection cheaper
### Memory overhead of objects
Many things will be stored by iD. With the graph structure in place, we'll
be storing much more.
## Connection, Graph, Map
The Map is a display and manipulation element. It should have minimal particulars
of how exactly to store or retrieve data. It gets data from Connection and
asks for it from Graph.
Graph stores all of the objects and all of the versions of those objects.
Connection requests objects over HTTP, parses them, and provides them to Graph.
## loaded

View File

@@ -175,6 +175,7 @@ table td {
.inspector-wrap a.permalink {
text-decoration:none;
margin-right:2em;
font: normal 11px/20px 'Helvetica'
}

View File

@@ -63,16 +63,10 @@
Imagery <a href="http://opengeodata.org/microsoft-imagery-details">&copy; 2012</a> Bing, GeoEye, Getmapping, Intermap, Microsoft.</p>
</div>
<script>
var connection = new iD.Connection(new iD.Graph());
var m = d3.select('#map');
// Initialise map
var map = iD.Map({
selector: "#map",
connection: connection,
width: m.node().offsetWidth,
height: m.node().offsetHeight
});
var m = d3.select('#map');
var map = iD.Map('#map');
map.setZoom(18)
.setCentre({ lat: 40.7965621, lon: -74.4773184 });
iD.Hash().map(map);

View File

@@ -1,6 +1,4 @@
iD.Connection = function(graph) {
// summary: The data store, including methods to fetch data from (and, eventually, save data to)
// an OSM API server.
var nextNode = -1,
nextWay = -1,
nextRelation = -1,
@@ -49,22 +47,15 @@ iD.Connection = function(graph) {
}
function getMembers(obj) {
var members = [];
var elems = obj.getElementsByTagName('member');
var members = [],
elems = obj.getElementsByTagName('member');
for (var i = 0; i < elems.length; i++) {
var item = elems[i];
var id = +item.attributes.ref.nodeValue,
type = item.attributes.type.nodeValue,
role = item.attributes.role.nodeValue;
var o = {
id: id,
type: type,
role: role
};
members.push(o);
members.push({
id: +elems[i].attributes.ref.nodeValue,
type: elems[i].attributes.type.nodeValue,
role: elems[i].attributes.role.nodeValue
});
}
return members;
@@ -87,9 +78,10 @@ iD.Connection = function(graph) {
if (!dom.childNodes) {
return callback(new Error('Bad request'));
}
var ways = dom.childNodes[0].getElementsByTagName('way'),
relations = dom.childNodes[0].getElementsByTagName('relation'),
nodes = dom.childNodes[0].getElementsByTagName('node');
var root = dom.childNodes[0],
ways = root.getElementsByTagName('way'),
relations = root.getElementsByTagName('relation'),
nodes = root.getElementsByTagName('node');
var i;
for (i = 0; i < ways.length; i++) graph.insert(objectData(ways[i]));
for (i = 0; i < relations.length; i++) graph.insert(objectData(relations[i]));

View File

@@ -21,7 +21,7 @@ iD.GeoJSON = {
properties: entity.tags,
geometry: {
'type': 'LineString',
'coordinates': _.map(entity.nodes, function(node) {
'coordinates': _.map(entity.children, function(node) {
return [node.lon, node.lat];
})
}

View File

@@ -1,6 +1,5 @@
iD.Graph = function() {
this.index = {};
this.nodes = [];
};
iD.Graph.prototype = {
@@ -26,5 +25,15 @@ iD.Graph.prototype = {
if (obj && (!this.index[obj.id] || !this.index[obj.id].loaded)) {
this.index[obj.id] = obj;
}
},
fetch: function(id) {
var obj = _.clone(this.index[id]);
var children = [];
for (var i = 0; i < obj.children.length; i++) {
children.push(this.index[obj.children[i]]);
}
obj.children = children;
return obj;
}
};

View File

@@ -4,6 +4,7 @@ iD.Node = function(id, lat, lon, tags, loaded) {
this.lat = lat;
this.lon = lon;
this.tags = tags || {};
this.children = [];
this.loaded = (loaded === undefined) ? true : loaded;
};
@@ -20,4 +21,4 @@ iD.Node.prototype = {
};
iD.Util.extend(iD.Node, iD.Entity);
// iD.Util.extend(iD.Node, iD.Entity);

View File

@@ -12,9 +12,4 @@ iD.Relation.prototype = {
intersects: function() { return true; }
};
iD.Util.extend(iD.Relation, iD.Entity);
iD.RelationMember = function(entity, role) {
this.entity = entity;
this.role = role;
};
// iD.Util.extend(iD.Relation, iD.Entity);

View File

@@ -38,3 +38,11 @@ iD.Util.extend = function(child, parent) {
}
return child;
};
iD.Util.codeWindow = function(content) {
top.win = window.open('','contentWindow',
'width=350,height=350,menubar=0' +
',toolbar=1,status=0,scrollbars=1,resizable=1');
top.win.document.writeln('<pre>' + content + '</pre>');
top.win.document.close();
};

View File

@@ -65,4 +65,4 @@ iD.Way.prototype = {
}
};
iD.Util.extend(iD.Way, iD.Entity);
// iD.Util.extend(iD.Way, iD.Entity);

View File

@@ -4,18 +4,30 @@
// ----------------------------------------------------------------------
// Connection base class
iD.Map = function(obj) {
var map = {},
iD.Map = function(parentSelector) {
var graph = new iD.Graph(),
connection = new iD.Connection(graph);
map = {},
parent = d3.selectAll(parentSelector),
selection = [],
width = obj.width || 800,
height = obj.height || 400,
width = parent.node().offsetWidth,
height = parent.node().offsetHeight,
projection = d3.geo.mercator()
.scale(512).translate([512, 512]),
connection = obj.connection;
dispatch = d3.dispatch('move');
var event = d3.dispatch('move');
var zoombehavior = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([256, 134217728]),
dragbehavior = d3.behavior.drag()
.origin(function(d) {
var p = projection(d);
return { x: p[0], y: p[1] };
})
.on("drag", dragmove);
var inspector = iD.Inspector();
var inspector = iD.Inspector(graph);
var linegen = d3.svg.line()
.x(function(d) {
@@ -27,33 +39,20 @@ iD.Map = function(obj) {
return projection([node.lon, node.lat])[1];
});
var zoombehavior = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([256, 134217728]);
var dragbehavior = d3.behavior.drag()
.origin(function(d) {
var p = projection(d);
return { x: p[0], y: p[1] };
})
.on("drag", dragmove);
// http://bl.ocks.org/1557377
function dragmove(d) {
d3.select(this).attr('transform', function() {
return 'translate(' + d3.event.x + ',' + d3.event.y + ')';
});
var ll = projection.invert([d3.event.x, d3.event.y]);
d[0] = ll[0];
d[1] = ll[1];
d.lon = ll[0];
d.lat = ll[1];
drawVector();
}
zoombehavior.on('zoom', redraw);
var surface = d3.selectAll(obj.selector)
.append('svg')
var surface = parent.append('svg')
.call(zoombehavior);
var defs = surface.append('defs');
@@ -214,15 +213,6 @@ iD.Map = function(obj) {
.attr('transform', function(d) {
return 'translate(' + projection(d) + ')';
});
}
function zoomIn() {
return setZoom(getZoom() + 1);
}
function zoomOut() {
return setZoom(getZoom() - 1);
}
function getZoom(zoom) {
@@ -239,13 +229,16 @@ iD.Map = function(obj) {
return map;
}
function zoomIn() { return setZoom(getZoom() + 1); }
function zoomOut() { return setZoom(getZoom() - 1); }
function redraw() {
if (d3.event) {
projection
.translate(d3.event.translate)
.scale(d3.event.scale);
}
event.move(map);
dispatch.move(map);
tileclient.redraw();
drawVector();
download();
@@ -275,10 +268,12 @@ iD.Map = function(obj) {
map.download = download;
map.extent = extent;
map.setCentre = setCentre;
map.setCenter = setCentre;
map.getCentre = getCenter;
map.getCenter = getCenter;
map.getZoom = getZoom;
map.setZoom = setZoom;
map.zoomIn = zoomIn;
@@ -290,5 +285,5 @@ iD.Map = function(obj) {
setSize(width, height);
redraw();
return d3.rebind(map, event, 'on');
return d3.rebind(map, dispatch, 'on');
};

View File

@@ -1,105 +0,0 @@
// 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) {
// summary: Singleton-like class for POI drag and drop. Loads a config file (draganddrop.json)
// on initialisation, then populates the drop-down palette with it.
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) {
// summary: Draw the grid of icons based on the JSON-derived object loaded.
var row;
for (var i=0; i<obj.length; i++) {
var item=obj[i];
if (!row || row.cells.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) {
// summary: Handler for dragging over the map; tells the browser that it's droppable here.
this.dragevent=event;
event.preventDefault();
},
end:function(event) {
// summary: Create a new POI from the dropped icon.
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) {
// summary: Turn a string of format k1=v1;k2=v2;... into a hash.
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
});

View File

@@ -1,4 +1,4 @@
iD.Inspector = function(selection) {
iD.Inspector = function(graph) {
var event = d3.dispatch('change', 'update');
function inspector(selection) {
@@ -20,6 +20,16 @@ iD.Inspector = function(selection) {
d.type + '/' + d.id)
.text('#' + d.id);
head.append('a')
.attr('class', 'permalink')
.attr('href', '#')
.text(d.id + '.geojson')
.on('click', function() {
iD.Util.codeWindow(
JSON.stringify(iD.GeoJSON.mapping(graph.fetch(d.id)), null, 2));
d3.event.stopPropagation();
});
var table = d3.select(this)
.append('table')
.attr('class', 'inspector');

View File

@@ -1,8 +1,8 @@
describe('Relation Member', function() {
describe('Relation', function() {
var rm;
beforeEach(function() {
rm = new iD.RelationMember();
rm = new iD.Relation();
});
it('is instantiated', function() {