diff --git a/css/map.css b/css/map.css
index 3dd66afae..f00f64431 100644
--- a/css/map.css
+++ b/css/map.css
@@ -567,6 +567,50 @@ text.tag-oneway {
pointer-events:none;
}
+/*
+ * Labels
+ */
+
+.layer-halo path {
+ point-events: none;
+ stroke-linecap: round;
+ stroke-linejoin: bevel;
+ stroke-width: 20px;
+ opacity: 0.8;
+ stroke: white;
+}
+
+text.arealabel,
+text.pathlabel,
+text.pointlabel {
+ font-size: 12px;
+ font-weight: bold;
+ fill: black;
+ text-anchor: middle;
+ pointer-events: none;
+}
+
+.pathlabel .textpath {
+ dominant-baseline: middle;
+}
+
+.pointlabel-halo,
+.linelabel-halo,
+.arealabel-halo {
+ opacity: 0.7;
+ pointer-events: none;
+}
+
+
+text.area.tag-leisure-park {
+ font-size: 16px;
+}
+
+text.point.tag-shop,
+text.point.tag-amenity {
+ font-size: 9px;
+}
+
/* Cursors */
#map:hover {
diff --git a/index.html b/index.html
index da369bfe7..19f2f31a6 100644
--- a/index.html
+++ b/index.html
@@ -26,6 +26,7 @@
+
@@ -50,6 +51,7 @@
+
diff --git a/js/id/geo.js b/js/id/geo.js
index b05a185fa..08df659b0 100644
--- a/js/id/geo.js
+++ b/js/id/geo.js
@@ -73,3 +73,14 @@ iD.geo.polygonIntersectsPolygon = function(outer, inner) {
return iD.geo.pointInPolygon(point, outer);
});
};
+
+iD.geo.pathLength = function(path) {
+ var length = 0,
+ dx, dy;
+ for (var i = 0; i < path.length - 1; i++) {
+ dx = path[i][0] - path[i + 1][0];
+ dy = path[i][1] - path[i + 1][1];
+ length += Math.sqrt(dx * dx + dy * dy);
+ }
+ return length;
+};
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 28b60c385..f283b6f40 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -22,6 +22,7 @@ iD.Map = function() {
areas = iD.svg.Areas(roundedProjection),
multipolygons = iD.svg.Multipolygons(roundedProjection),
midpoints = iD.svg.Midpoints(roundedProjection),
+ labels = iD.svg.Labels(roundedProjection),
tail = d3.tail(),
surface, tilegroup;
@@ -101,7 +102,8 @@ iD.Map = function() {
.call(lines, graph, all, filter)
.call(areas, graph, all, filter)
.call(multipolygons, graph, all, filter)
- .call(midpoints, graph, all, filter);
+ .call(midpoints, graph, all, filter)
+ .call(labels, graph, all, filter);
}
dispatch.drawn(map);
}
diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js
new file mode 100644
index 000000000..6dcd3ec4c
--- /dev/null
+++ b/js/id/svg/labels.js
@@ -0,0 +1,322 @@
+iD.svg.Labels = function(projection) {
+
+ // Replace with dict and iterate over entities tags instead?
+ var label_stack = [
+ ['line', 'highway'],
+ ['area', 'building', 'yes'],
+ ['area', 'leisure', 'park'],
+ ['area', 'natural'],
+ ['point', 'amenity'],
+ ['point', 'shop'],
+ ];
+
+ var default_size = 12;
+ var font_sizes = label_stack.map(function(d) {
+ var style = iD.util.getStyle(
+ 'text.' + d[0] + '.tag-' + d.slice(1).join('-'));
+ var m = style && style.cssText.match("font-size: ([0-9]{1,2})px;");
+ if (!m) return default_size;
+ return parseInt(m[1], 10);
+ });
+
+ var pointOffsets = [
+ [15, 3, 'start'], // right
+ [10, 0, 'start'], // unused right now
+ [-15, 0, 'end']
+ ];
+
+ var lineOffsets = [
+ 50, 40, 60, 30, 70
+ ];
+
+ function get(array, prop) {
+ return function(d, i) { return array[i][prop] };
+ }
+
+ var textWidthCache = {};
+ function textWidth(text, size, elem) {
+ var c = textWidthCache[size];
+ if (!c) c = textWidthCache[size] = {};
+ if (c[text]) return c[text];
+ else if (elem) return c[text] = elem.getComputedTextLength();
+ else return size / 3 * 2 * text.length;
+ }
+
+ function drawLineLabels(group, entities, filter, classes, labels) {
+
+ var texts = group.selectAll('text.' + classes)
+ .filter(filter)
+ .data(entities, iD.Entity.key)
+
+ var tp = texts.enter()
+ .append('text')
+ .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'];})
+ .append('textPath')
+ .attr('class', 'textpath');
+
+
+ var tps = texts.selectAll('.textpath')
+ .filter(filter)
+ .data(entities, iD.Entity.key)
+ .attr({
+ 'startOffset': '50%',
+ 'xlink:href': function(d, i) { return '#halo-' + d.id}
+ })
+ .text(function(d, i) { return d.tags.name });
+
+ texts.exit().remove();
+
+ }
+
+ function drawLineHalos(group, entities, filter, classes, labels) {
+
+ var halos = group.selectAll('path')
+ .filter(filter)
+ .data(entities, iD.Entity.key);
+
+ halos.enter()
+ .append('path')
+ .style('stroke-width', get(labels, 'font-size'))
+ .attr('id', function(d, i) { return 'halo-' + d.id })
+ .attr('class', classes);
+
+ halos.attr('d', get(labels, 'lineString'));
+
+ halos.exit().remove();
+ }
+
+ function drawPointHalos(group, entities, filter, classes, labels) {
+
+ var halos = group.selectAll('rect.' + classes)
+ .filter(filter)
+ .data(entities, iD.Entity.key);
+
+ halos.enter()
+ .append('rect')
+ .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'];});
+
+ halos.attr({
+ 'x': function(d, i) {
+ var x = labels[i]['x'] - 2;
+ if (labels[i]['textAnchor'] === 'middle') {
+ x -= textWidth(d.tags.name, labels[i]['height']) / 2;
+ }
+ return x;
+ },
+ 'y': function(d, i) { return labels[i]['y'] - labels[i]['height'] + 1 - 2; },
+ 'rx': 3,
+ 'ry': 3,
+ 'width': function(d, i) { return textWidth(d.tags.name, labels[i]['height']) + 4 },
+ 'height': function(d, i) { return labels[i]['height'] + 4 },
+ 'fill': 'white',
+ });
+
+ halos.exit().remove();
+ }
+
+
+ function drawPointLabels(group, entities, filter, classes, labels) {
+
+ var texts = group.selectAll('text.' + classes)
+ .filter(filter)
+ .data(entities, iD.Entity.key);
+
+ texts.enter()
+ .append('text')
+ .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'] })
+
+ texts.attr('x', get(labels, 'x'))
+ .attr('y', get(labels, 'y'))
+ .attr('transform', get(labels, 'transform'))
+ .style('text-anchor', get(labels, 'textAnchor'))
+ .text(function(d) { return d.tags.name })
+ .each(function(d, i) { textWidth(d.tags.name, labels[i]['height'], this); });
+
+ texts.exit().remove();
+ return texts;
+ }
+
+ function reverse(p) {
+ var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]),
+ reverse = !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > - Math.PI/2);
+ return reverse;
+ }
+
+ function lineString(nodes) {
+ return 'M' + nodes.join('L');
+ }
+
+ function subpath(nodes, from, to) {
+ function segmentLength(i) {
+ var dx = nodes[i][0] - nodes[i + 1][0];
+ var dy = nodes[i][1] - nodes[i + 1][1];
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ var sofar = 0,
+ start, end, i0, i1;
+ for (var i = 0; i < nodes.length - 1; i++) {
+ var current = segmentLength(i);
+ if (!start && sofar + current > from) {
+ var portion = (from - sofar) / current;
+ start = [
+ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
+ nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
+ ];
+ i0 = i + 1;
+ }
+ if (!end && sofar + current > to) {
+ var portion = (to - sofar) / current;
+ end = [
+ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
+ nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1])
+ ];
+ i1 = i + 1;
+ }
+ sofar += current;
+
+ }
+ var ret = nodes.slice(i0, i1);
+ ret.unshift(start);
+ ret.push(end);
+ return ret;
+
+ }
+
+
+ return function drawLabels(surface, graph, entities, filter) {
+
+ var rtree = new RTree();
+ var hidePoints = !d3.select('.node.point').node();
+
+ var labelable = [];
+ for (var i = 0; i < label_stack.length; i++) labelable.push([]);
+
+ // Split entities into groups specified by label_stack
+ for (var i = 0; i < entities.length; i++) {
+ var entity = entities[i];
+ if (!entity.tags.name) continue;
+ if (hidePoints && entity.geometry() === 'point') continue;
+ for (var k = 0; k < label_stack.length; k ++) {
+ if (entity.geometry() === label_stack[k][0] &&
+ entity.tags[label_stack[k][1]] && !entity.tags[label_stack[k][2]]) {
+ labelable[k].push(entity);
+ break;
+ }
+ }
+ }
+
+
+ var positions = {
+ point: [],
+ line: [],
+ area: []
+ };
+
+ var labelled = {
+ point: [],
+ line: [],
+ area: []
+ };
+
+ // Try and find a valid label for labellable entities
+ for (var k = 0; k < labelable.length; k++) {
+ var font_size = font_sizes[k];
+ for (var i = 0; i < labelable[k].length; i ++) {
+ var entity = labelable[k][i],
+ width = textWidth(entity.tags.name, font_size),
+ p;
+ if (entity.geometry() === 'point') {
+ p = getPointLabel(entity, width, font_size);
+ } else if (entity.geometry() === 'line') {
+ p = getLineLabel(entity, width, font_size);
+ } else if (entity.geometry() === 'area') {
+ p = getAreaLabel(entity, width, font_size);
+ }
+ if (p) {
+ p.classes = entity.geometry() + ' tag-' + label_stack[k].slice(1).join('-');
+ positions[entity.geometry()].push(p);
+ labelled[entity.geometry()].push(entity);
+ }
+ }
+ }
+
+ function getPointLabel(entity, width, height) {
+ var coord = projection(entity.loc),
+ m = 5, // margin
+ offset = pointOffsets[0],
+ p = {
+ height: height,
+ width: width,
+ x: coord[0] + offset[0],
+ y: coord[1] + offset[1],
+ textAnchor: offset[2]
+ }
+ var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m);
+ if (tryInsert(rect)) return p;
+ }
+
+ function getLineLabel(entity, width, height) {
+ var nodes = _.pluck(entity.nodes, 'loc').map(projection),
+ length = iD.geo.pathLength(nodes);
+ if (length < width + 20) return;
+
+ // 50, 40, 60, 30, 70
+ for (var i = 0; i < 5; i ++) {
+ var offset = lineOffsets[i],
+ middle = offset / 100 * length;
+ if (middle <= width / 2) return;
+ var start = middle - width/2,
+ sub = subpath(nodes, start, start + width),
+ rev = reverse(sub),
+ rect = new RTree.Rectangle(
+ Math.min(sub[0][0], sub[sub.length - 1][0]) - 10,
+ Math.min(sub[0][1], sub[sub.length - 1][1]) - 10,
+ Math.abs(sub[0][0] - sub[sub.length - 1][0]) + 20,
+ Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30
+ );
+ if (rev) sub = sub.reverse();
+ if (tryInsert(rect)) return {
+ 'font-size': height + 2,
+ lineString: lineString(sub),
+ startOffset: offset + '%'
+ }
+ }
+ }
+
+ function getAreaLabel(entity, width, height) {
+ var nodes = _.pluck(entity.nodes, 'loc')
+ .map(iD.svg.RoundProjection(projection)),
+ centroid = d3.geom.polygon(nodes).centroid(),
+ extent = entity.extent(graph),
+ entitywidth = projection(extent[1])[0] - projection(extent[0])[0];
+
+ if (entitywidth < width + 20) return;
+ var p = {
+ x: centroid[0],
+ y: centroid[1],
+ textAnchor: 'middle',
+ height: height
+ }
+ var rect = new RTree.Rectangle(p.x - width/2, p.y, width, height);
+ if (tryInsert(rect)) return p;
+
+ }
+
+ function tryInsert(rect) {
+ var v = rtree.search(rect, true).length === 0;
+ if (v) rtree.insert(rect);
+ return v;
+ }
+
+ var label = surface.select('.layer-label'),
+ halo = surface.select('.layer-halo'),
+ points = drawPointLabels(label, labelled['point'], filter, 'pointlabel', positions['point']),
+ pointHalos = drawPointHalos(halo, labelled['point'], filter, 'pointlabel-halo', positions['point']),
+ linesHalos = drawLineHalos(halo, labelled['line'], filter, 'linelabel-halo', positions['line']),
+ lines = drawLineLabels(label, labelled['line'], filter, 'pathlabel', positions['line']),
+ areas = drawPointLabels(label, labelled['area'], filter, 'arealabel', positions['area']),
+ areaHalos = drawPointHalos(halo, labelled['area'], filter, 'arealabel-halo', positions['area']);
+ };
+
+};
diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js
index 57b981dc5..7561802e5 100644
--- a/js/id/svg/surface.js
+++ b/js/id/svg/surface.js
@@ -3,7 +3,7 @@ iD.svg.Surface = function() {
selection.append('defs');
var layers = selection.selectAll('.layer')
- .data(['shadow', 'fill', 'casing', 'stroke', 'text', 'hit']);
+ .data(['shadow', 'fill', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']);
layers.enter().append('g')
.attr('class', function(d) { return 'layer layer-' + d; });
diff --git a/js/id/util.js b/js/id/util.js
index 2e6e3453d..a9ccd104f 100644
--- a/js/id/util.js
+++ b/js/id/util.js
@@ -70,3 +70,15 @@ iD.util.prefixCSSProperty = function(property) {
return false;
};
+
+iD.util.getStyle = function(selector) {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ var rules = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
+ for (var k = 0; k < rules.length; k++) {
+ var selectorText = rules[k].selectorText && rules[k].selectorText.split(', ');
+ if (_.contains(selectorText, selector)) {
+ return rules[k];
+ }
+ }
+ }
+};
diff --git a/js/lib/rtree.js b/js/lib/rtree.js
new file mode 100644
index 000000000..7f3b4a461
--- /dev/null
+++ b/js/lib/rtree.js
@@ -0,0 +1,711 @@
+/******************************************************************************
+ rtree.js - General-Purpose Non-Recursive Javascript R-Tree Library
+ Version 0.6.2, December 5st 2009
+
+@license Copyright (c) 2009 Jon-Carlos Rivera
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ Jon-Carlos Rivera - imbcmdth@hotmail.com
+******************************************************************************/
+
+/**
+ * RTree - A simple r-tree structure for great results.
+ * @constructor
+ */
+var RTree = function(width){
+ // Variables to control tree-dimensions
+ var _Min_Width = 3; // Minimum width of any node before a merge
+ var _Max_Width = 6; // Maximum width of any node before a split
+ if(!isNaN(width)){ _Min_Width = Math.floor(width/2.0); _Max_Width = width;}
+ // Start with an empty root-tree
+ var _T = {x:0, y:0, w:0, h:0, id:"root", nodes:[] };
+
+ var isArray = function(o) {
+ return Object.prototype.toString.call(o) === '[object Array]';
+ };
+
+ /**@function
+ * @description Function to generate unique strings for element IDs
+ * @param {String} n The prefix to use for the IDs generated.
+ * @return {String} A guarenteed unique ID.
+ */
+ var _name_to_id = (function() {
+ // hide our idCache inside this closure
+ var idCache = {};
+
+ // return the api: our function that returns a unique string with incrementing number appended to given idPrefix
+ return function(idPrefix) {
+ var idVal = 0;
+ if(idPrefix in idCache) {
+ idVal = idCache[idPrefix]++;
+ } else {
+ idCache[idPrefix] = 0;
+ }
+ return idPrefix + "_" + idVal;
+ }
+ })();
+
+ // This is my special addition to the world of r-trees
+ // every other (simple) method I found produced crap trees
+ // this skews insertions to prefering squarer and emptier nodes
+ RTree.Rectangle.squarified_ratio = function(l, w, fill) {
+ // Area of new enlarged rectangle
+ var lperi = (l + w) / 2.0; // Average size of a side of the new rectangle
+ var larea = l * w; // Area of new rectangle
+ // return the ratio of the perimeter to the area - the closer to 1 we are,
+ // the more "square" a rectangle is. conversly, when approaching zero the
+ // more elongated a rectangle is
+ var lgeo = larea / (lperi*lperi);
+ return(larea * fill / lgeo);
+ };
+
+ /**find the best specific node(s) for object to be deleted from
+ * [ leaf node parent ] = _remove_subtree(rectangle, object, root)
+ * @private
+ */
+ var _remove_subtree = function(rect, obj, root) {
+ var hit_stack = []; // Contains the elements that overlap
+ var count_stack = []; // Contains the elements that overlap
+ var ret_array = [];
+ var current_depth = 1;
+
+ if(!rect || !RTree.Rectangle.overlap_rectangle(rect, root))
+ return ret_array;
+
+ var ret_obj = {x:rect.x, y:rect.y, w:rect.w, h:rect.h, target:obj};
+
+ count_stack.push(root.nodes.length);
+ hit_stack.push(root);
+
+ do {
+ var tree = hit_stack.pop();
+ var i = count_stack.pop()-1;
+
+ if("target" in ret_obj) { // We are searching for a target
+ while(i >= 0) {
+ var ltree = tree.nodes[i];
+ if(RTree.Rectangle.overlap_rectangle(ret_obj, ltree)) {
+ if( (ret_obj.target && "leaf" in ltree && ltree.leaf === ret_obj.target)
+ ||(!ret_obj.target && ("leaf" in ltree || RTree.Rectangle.contains_rectangle(ltree, ret_obj)))) { // A Match !!
+ // Yup we found a match...
+ // we can cancel search and start walking up the list
+ if("nodes" in ltree) {// If we are deleting a node not a leaf...
+ ret_array = _search_subtree(ltree, true, [], ltree);
+ tree.nodes.splice(i, 1);
+ } else {
+ ret_array = tree.nodes.splice(i, 1);
+ }
+ // Resize MBR down...
+ RTree.Rectangle.make_MBR(tree.nodes, tree);
+ delete ret_obj.target;
+ if(tree.nodes.length < _Min_Width) { // Underflow
+ ret_obj.nodes = _search_subtree(tree, true, [], tree);
+ }
+ break;
+ }/* else if("load" in ltree) { // A load
+ }*/ else if("nodes" in ltree) { // Not a Leaf
+ current_depth += 1;
+ count_stack.push(i);
+ hit_stack.push(tree);
+ tree = ltree;
+ i = ltree.nodes.length;
+ }
+ }
+ i -= 1;
+ }
+ } else if("nodes" in ret_obj) { // We are unsplitting
+ tree.nodes.splice(i+1, 1); // Remove unsplit node
+ // ret_obj.nodes contains a list of elements removed from the tree so far
+ if(tree.nodes.length > 0)
+ RTree.Rectangle.make_MBR(tree.nodes, tree);
+ for(var t = 0;t 0 && tree.nodes.length < _Min_Width) { // Underflow..AGAIN!
+ ret_obj.nodes = _search_subtree(tree, true, ret_obj.nodes, tree);
+ tree.nodes.length = 0;
+ }else {
+ delete ret_obj.nodes; // Just start resizing
+ }
+ } else { // we are just resizing
+ RTree.Rectangle.make_MBR(tree.nodes, tree);
+ }
+ current_depth -= 1;
+ }while(hit_stack.length > 0);
+
+ return(ret_array);
+ };
+
+ /**choose the best damn node for rectangle to be inserted into
+ * [ leaf node parent ] = _choose_leaf_subtree(rectangle, root to start search at)
+ * @private
+ */
+ var _choose_leaf_subtree = function(rect, root) {
+ var best_choice_index = -1;
+ var best_choice_stack = [];
+ var best_choice_area;
+
+ var load_callback = function(local_tree, local_node){
+ return(function(data) {
+ local_tree._attach_data(local_node, data);
+ });
+ };
+
+ best_choice_stack.push(root);
+ var nodes = root.nodes;
+
+ do {
+ if(best_choice_index != -1) {
+ best_choice_stack.push(nodes[best_choice_index]);
+ nodes = nodes[best_choice_index].nodes;
+ best_choice_index = -1;
+ }
+
+ for(var i = nodes.length-1; i >= 0; i--) {
+ var ltree = nodes[i];
+ if("leaf" in ltree) {
+ // Bail out of everything and start inserting
+ best_choice_index = -1;
+ break;
+ } /*else if(ltree.load) {
+ throw( "Can't insert into partially loaded tree ... yet!");
+ //jQuery.getJSON(ltree.load, load_callback(this, ltree));
+ //delete ltree.load;
+ }*/
+ // Area of new enlarged rectangle
+ var old_lratio = RTree.Rectangle.squarified_ratio(ltree.w, ltree.h, ltree.nodes.length+1);
+
+ // Enlarge rectangle to fit new rectangle
+ var nw = Math.max(ltree.x+ltree.w, rect.x+rect.w) - Math.min(ltree.x, rect.x);
+ var nh = Math.max(ltree.y+ltree.h, rect.y+rect.h) - Math.min(ltree.y, rect.y);
+
+ // Area of new enlarged rectangle
+ var lratio = RTree.Rectangle.squarified_ratio(nw, nh, ltree.nodes.length+2);
+
+ if(best_choice_index < 0 || Math.abs(lratio - old_lratio) < best_choice_area) {
+ best_choice_area = Math.abs(lratio - old_lratio); best_choice_index = i;
+ }
+ }
+ }while(best_choice_index != -1);
+
+ return(best_choice_stack);
+ };
+
+ /**split a set of nodes into two roughly equally-filled nodes
+ * [ an array of two new arrays of nodes ] = linear_split(array of nodes)
+ * @private
+ */
+ var _linear_split = function(nodes) {
+ var n = _pick_linear(nodes);
+ while(nodes.length > 0) {
+ _pick_next(nodes, n[0], n[1]);
+ }
+ return(n);
+ };
+
+ /**insert the best source rectangle into the best fitting parent node: a or b
+ * [] = pick_next(array of source nodes, target node array a, target node array b)
+ * @private
+ */
+ var _pick_next = function(nodes, a, b) {
+ // Area of new enlarged rectangle
+ var area_a = RTree.Rectangle.squarified_ratio(a.w, a.h, a.nodes.length+1);
+ var area_b = RTree.Rectangle.squarified_ratio(b.w, b.h, b.nodes.length+1);
+ var high_area_delta;
+ var high_area_node;
+ var lowest_growth_group;
+
+ for(var i = nodes.length-1; i>=0;i--) {
+ var l = nodes[i];
+ var new_area_a = {};
+ new_area_a.x = Math.min(a.x, l.x); new_area_a.y = Math.min(a.y, l.y);
+ new_area_a.w = Math.max(a.x+a.w, l.x+l.w) - new_area_a.x; new_area_a.h = Math.max(a.y+a.h, l.y+l.h) - new_area_a.y;
+ var change_new_area_a = Math.abs(RTree.Rectangle.squarified_ratio(new_area_a.w, new_area_a.h, a.nodes.length+2) - area_a);
+
+ var new_area_b = {};
+ new_area_b.x = Math.min(b.x, l.x); new_area_b.y = Math.min(b.y, l.y);
+ new_area_b.w = Math.max(b.x+b.w, l.x+l.w) - new_area_b.x; new_area_b.h = Math.max(b.y+b.h, l.y+l.h) - new_area_b.y;
+ var change_new_area_b = Math.abs(RTree.Rectangle.squarified_ratio(new_area_b.w, new_area_b.h, b.nodes.length+2) - area_b);
+
+ if( !high_area_node || !high_area_delta || Math.abs( change_new_area_b - change_new_area_a ) < high_area_delta ) {
+ high_area_node = i;
+ high_area_delta = Math.abs(change_new_area_b-change_new_area_a);
+ lowest_growth_group = change_new_area_b < change_new_area_a ? b : a;
+ }
+ }
+ var temp_node = nodes.splice(high_area_node, 1)[0];
+ if(a.nodes.length + nodes.length + 1 <= _Min_Width) {
+ a.nodes.push(temp_node);
+ RTree.Rectangle.expand_rectangle(a, temp_node);
+ } else if(b.nodes.length + nodes.length + 1 <= _Min_Width) {
+ b.nodes.push(temp_node);
+ RTree.Rectangle.expand_rectangle(b, temp_node);
+ }
+ else {
+ lowest_growth_group.nodes.push(temp_node);
+ RTree.Rectangle.expand_rectangle(lowest_growth_group, temp_node);
+ }
+ };
+
+ /**pick the "best" two starter nodes to use as seeds using the "linear" criteria
+ * [ an array of two new arrays of nodes ] = pick_linear(array of source nodes)
+ * @private
+ */
+ var _pick_linear = function(nodes) {
+ var lowest_high_x = nodes.length-1;
+ var highest_low_x = 0;
+ var lowest_high_y = nodes.length-1;
+ var highest_low_y = 0;
+ var t1, t2;
+
+ for(var i = nodes.length-2; i>=0;i--) {
+ var l = nodes[i];
+ if(l.x > nodes[highest_low_x].x ) highest_low_x = i;
+ else if(l.x+l.w < nodes[lowest_high_x].x+nodes[lowest_high_x].w) lowest_high_x = i;
+ if(l.y > nodes[highest_low_y].y ) highest_low_y = i;
+ else if(l.y+l.h < nodes[lowest_high_y].y+nodes[lowest_high_y].h) lowest_high_y = i;
+ }
+ var dx = Math.abs((nodes[lowest_high_x].x+nodes[lowest_high_x].w) - nodes[highest_low_x].x);
+ var dy = Math.abs((nodes[lowest_high_y].y+nodes[lowest_high_y].h) - nodes[highest_low_y].y);
+ if( dx > dy ) {
+ if(lowest_high_x > highest_low_x) {
+ t1 = nodes.splice(lowest_high_x, 1)[0];
+ t2 = nodes.splice(highest_low_x, 1)[0];
+ } else {
+ t2 = nodes.splice(highest_low_x, 1)[0];
+ t1 = nodes.splice(lowest_high_x, 1)[0];
+ }
+ } else {
+ if(lowest_high_y > highest_low_y) {
+ t1 = nodes.splice(lowest_high_y, 1)[0];
+ t2 = nodes.splice(highest_low_y, 1)[0];
+ } else {
+ t2 = nodes.splice(highest_low_y, 1)[0];
+ t1 = nodes.splice(lowest_high_y, 1)[0];
+ }
+ }
+ return([{x:t1.x, y:t1.y, w:t1.w, h:t1.h, nodes:[t1]},
+ {x:t2.x, y:t2.y, w:t2.w, h:t2.h, nodes:[t2]} ]);
+ };
+
+ var _attach_data = function(node, more_tree){
+ node.nodes = more_tree.nodes;
+ node.x = more_tree.x; node.y = more_tree.y;
+ node.w = more_tree.w; node.h = more_tree.h;
+ return(node);
+ };
+
+ /**non-recursive internal search function
+ * [ nodes | objects ] = _search_subtree(rectangle, [return node data], [array to fill], root to begin search at)
+ * @private
+ */
+ var _search_subtree = function(rect, return_node, return_array, root) {
+ var hit_stack = []; // Contains the elements that overlap
+
+ if(!RTree.Rectangle.overlap_rectangle(rect, root))
+ return(return_array);
+
+ var load_callback = function(local_tree, local_node){
+ return(function(data) {
+ local_tree._attach_data(local_node, data);
+ });
+ };
+
+ hit_stack.push(root.nodes);
+
+ do {
+ var nodes = hit_stack.pop();
+
+ for(var i = nodes.length-1; i >= 0; i--) {
+ var ltree = nodes[i];
+ if(RTree.Rectangle.overlap_rectangle(rect, ltree)) {
+ if("nodes" in ltree) { // Not a Leaf
+ hit_stack.push(ltree.nodes);
+ } else if("leaf" in ltree) { // A Leaf !!
+ if(!return_node)
+ return_array.push(ltree.leaf);
+ else
+ return_array.push(ltree);
+ }/* else if("load" in ltree) { // We need to fetch a URL for some more tree data
+ jQuery.getJSON(ltree.load, load_callback(this, ltree));
+ delete ltree.load;
+ // i++; // Replay this entry
+ }*/
+ }
+ }
+ }while(hit_stack.length > 0);
+
+ return(return_array);
+ };
+
+ /**non-recursive internal insert function
+ * [] = _insert_subtree(rectangle, object to insert, root to begin insertion at)
+ * @private
+ */
+ var _insert_subtree = function(node, root) {
+ var bc; // Best Current node
+ // Initial insertion is special because we resize the Tree and we don't
+ // care about any overflow (seriously, how can the first object overflow?)
+ if(root.nodes.length == 0) {
+ root.x = node.x; root.y = node.y;
+ root.w = node.w; root.h = node.h;
+ root.nodes.push(node);
+ return;
+ }
+
+ // Find the best fitting leaf node
+ // choose_leaf returns an array of all tree levels (including root)
+ // that were traversed while trying to find the leaf
+ var tree_stack = _choose_leaf_subtree(node, root);
+ var ret_obj = node;//{x:rect.x,y:rect.y,w:rect.w,h:rect.h, leaf:obj};
+
+ // Walk back up the tree resizing and inserting as needed
+ do {
+ //handle the case of an empty node (from a split)
+ if(bc && "nodes" in bc && bc.nodes.length == 0) {
+ var pbc = bc; // Past bc
+ bc = tree_stack.pop();
+ for(var t=0;t 0);
+ };
+
+ /**quick 'n' dirty function for plugins or manually drawing the tree
+ * [ tree ] = RTree.get_tree(): returns the raw tree data. useful for adding
+ * @public
+ * !! DEPRECATED !!
+ */
+ this.get_tree = function() {
+ return _T;
+ };
+
+ /**quick 'n' dirty function for plugins or manually loading the tree
+ * [ tree ] = RTree.set_tree(sub-tree, where to attach): returns the raw tree data. useful for adding
+ * @public
+ * !! DEPRECATED !!
+ */
+ this.set_tree = function(new_tree, where) {
+ if(!where)
+ where = _T;
+ return(_attach_data(where, new_tree));
+ };
+
+ /**non-recursive search function
+ * [ nodes | objects ] = RTree.search(rectangle, [return node data], [array to fill])
+ * @public
+ */
+ this.search = function(rect, return_node, return_array) {
+ if(arguments.length < 1)
+ throw "Wrong number of arguments. RT.Search requires at least a bounding rectangle."
+
+ switch(arguments.length) {
+ case 1:
+ arguments[1] = false;// Add an "return node" flag - may be removed in future
+ case 2:
+ arguments[2] = []; // Add an empty array to contain results
+ case 3:
+ arguments[3] = _T; // Add root node to end of argument list
+ default:
+ arguments.length = 4;
+ }
+ return(_search_subtree.apply(this, arguments));
+ };
+
+ /**partially-recursive toJSON function
+ * [ string ] = RTree.toJSON([rectangle], [tree])
+ * @public
+ */
+ this.toJSON = function(rect, tree) {
+ var hit_stack = []; // Contains the elements that overlap
+ var count_stack = []; // Contains the elements that overlap
+ var return_stack = {}; // Contains the elements that overlap
+ var max_depth = 3; // This triggers recursion and tree-splitting
+ var current_depth = 1;
+ var return_string = "";
+
+ if(rect && !RTree.Rectangle.overlap_rectangle(rect, _T))
+ return "";
+
+ if(!tree) {
+ count_stack.push(_T.nodes.length);
+ hit_stack.push(_T.nodes);
+ return_string += "var main_tree = {x:"+_T.x.toFixed()+",y:"+_T.y.toFixed()+",w:"+_T.w.toFixed()+",h:"+_T.h.toFixed()+",nodes:[";
+ } else {
+ max_depth += 4;
+ count_stack.push(tree.nodes.length);
+ hit_stack.push(tree.nodes);
+ return_string += "var main_tree = {x:"+tree.x.toFixed()+",y:"+tree.y.toFixed()+",w:"+tree.w.toFixed()+",h:"+tree.h.toFixed()+",nodes:[";
+ }
+
+ do {
+ var nodes = hit_stack.pop();
+ var i = count_stack.pop()-1;
+
+ if(i >= 0 && i < nodes.length-1)
+ return_string += ",";
+
+ while(i >= 0) {
+ var ltree = nodes[i];
+ if(!rect || RTree.Rectangle.overlap_rectangle(rect, ltree)) {
+ if(ltree.nodes) { // Not a Leaf
+ if(current_depth >= max_depth) {
+ var len = return_stack.length;
+ var nam = _name_to_id("saved_subtree");
+ return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",load:'"+nam+".js'}";
+ return_stack[nam] = this.toJSON(rect, ltree);
+ if(i > 0)
+ return_string += ","
+ } else {
+ return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",nodes:[";
+ current_depth += 1;
+ count_stack.push(i);
+ hit_stack.push(nodes);
+ nodes = ltree.nodes;
+ i = ltree.nodes.length;
+ }
+ } else if(ltree.leaf) { // A Leaf !!
+ var data = ltree.leaf.toJSON ? ltree.leaf.toJSON() : JSON.stringify(ltree.leaf);
+ return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",leaf:" + data + "}";
+ if(i > 0)
+ return_string += ","
+ } else if(ltree.load) { // A load
+ return_string += "{x:"+ltree.x.toFixed()+",y:"+ltree.y.toFixed()+",w:"+ltree.w.toFixed()+",h:"+ltree.h.toFixed()+",load:'" + ltree.load + "'}";
+ if(i > 0)
+ return_string += ","
+ }
+ }
+ i -= 1;
+ }
+ if(i < 0) {
+ return_string += "]}"; current_depth -= 1;
+ }
+ }while(hit_stack.length > 0);
+
+ return_string+=";";
+
+ for(var my_key in return_stack) {
+ return_string += "\nvar " + my_key + " = function(){" + return_stack[my_key] + " return(main_tree);};";
+ }
+ return(return_string);
+ };
+
+ /**non-recursive function that deletes a specific
+ * [ number ] = RTree.remove(rectangle, obj)
+ */
+ this.remove = function(rect, obj) {
+ if(arguments.length < 1)
+ throw "Wrong number of arguments. RT.remove requires at least a bounding rectangle."
+
+ switch(arguments.length) {
+ case 1:
+ arguments[1] = false; // obj == false for conditionals
+ case 2:
+ arguments[2] = _T; // Add root node to end of argument list
+ default:
+ arguments.length = 3;
+ }
+ if(arguments[1] === false) { // Do area-wide delete
+ var numberdeleted = 0;
+ var ret_array = [];
+ do {
+ numberdeleted=ret_array.length;
+ ret_array = ret_array.concat(_remove_subtree.apply(this, arguments));
+ }while( numberdeleted != ret_array.length);
+ return ret_array;
+ }
+ else { // Delete a specific item
+ return(_remove_subtree.apply(this, arguments));
+ }
+ };
+
+ /**non-recursive insert function
+ * [] = RTree.insert(rectangle, object to insert)
+ */
+ this.insert = function(rect, obj) {
+/* if(arguments.length < 2)
+ throw "Wrong number of arguments. RT.Insert requires at least a bounding rectangle and an object."*/
+
+ return(_insert_subtree({x:rect.x,y:rect.y,w:rect.w,h:rect.h,leaf:obj}, _T));
+ };
+
+ /**non-recursive delete function
+ * [deleted object] = RTree.remove(rectangle, [object to delete])
+ */
+
+//End of RTree
+};
+
+/**Rectangle - Generic rectangle object - Not yet used */
+
+RTree.Rectangle = function(ix, iy, iw, ih) { // new Rectangle(bounds) or new Rectangle(x, y, w, h)
+ var x, x2, y, y2, w, h;
+
+ if(ix.x) {
+ x = ix.x; y = ix.y;
+ if(ix.w !== 0 && !ix.w && ix.x2){
+ w = ix.x2-ix.x; h = ix.y2-ix.y;
+ } else {
+ w = ix.w; h = ix.h;
+ }
+ x2 = x + w; y2 = y + h; // For extra fastitude
+ } else {
+ x = ix; y = iy; w = iw; h = ih;
+ x2 = x + w; y2 = y + h; // For extra fastitude
+ }
+
+ this.x1 = this.x = x;
+ this.y1 = this.y = y;
+ this.x2 = x2;
+ this.y2 = y2;
+ this.w = w;
+ this.h = h;
+
+ this.toJSON = function() {
+ return('{"x":'+x.toString()+', "y":'+y.toString()+', "w":'+w.toString()+', "h":'+h.toString()+'}');
+ };
+
+ this.overlap = function(a) {
+ return(this.x() < a.x2() && this.x2() > a.x() && this.y() < a.y2() && this.y2() > a.y());
+ };
+
+ this.expand = function(a) {
+ var nx = Math.min(this.x(), a.x());
+ var ny = Math.min(this.y(), a.y());
+ w = Math.max(this.x2(), a.x2()) - nx;
+ h = Math.max(this.y2(), a.y2()) - ny;
+ x = nx; y = ny;
+ return(this);
+ };
+
+ this.setRect = function(ix, iy, iw, ih) {
+ var x, x2, y, y2, w, h;
+ if(ix.x) {
+ x = ix.x; y = ix.y;
+ if(ix.w !== 0 && !ix.w && ix.x2) {
+ w = ix.x2-ix.x; h = ix.y2-ix.y;
+ } else {
+ w = ix.w; h = ix.h;
+ }
+ x2 = x + w; y2 = y + h; // For extra fastitude
+ } else {
+ x = ix; y = iy; w = iw; h = ih;
+ x2 = x + w; y2 = y + h; // For extra fastitude
+ }
+ };
+//End of RTree.Rectangle
+};
+
+
+/**returns true if rectangle 1 overlaps rectangle 2
+ * [ boolean ] = overlap_rectangle(rectangle a, rectangle b)
+ * @static function
+ */
+RTree.Rectangle.overlap_rectangle = function(a, b) {
+ return(a.x < (b.x+b.w) && (a.x+a.w) > b.x && a.y < (b.y+b.h) && (a.y+a.h) > b.y);
+};
+
+/**returns true if rectangle a is contained in rectangle b
+ * [ boolean ] = contains_rectangle(rectangle a, rectangle b)
+ * @static function
+ */
+RTree.Rectangle.contains_rectangle = function(a, b) {
+ return((a.x+a.w) <= (b.x+b.w) && a.x >= b.x && (a.y+a.h) <= (b.y+b.h) && a.y >= b.y);
+};
+
+/**expands rectangle A to include rectangle B, rectangle B is untouched
+ * [ rectangle a ] = expand_rectangle(rectangle a, rectangle b)
+ * @static function
+ */
+RTree.Rectangle.expand_rectangle = function(a, b) {
+ var nx = Math.min(a.x, b.x);
+ var ny = Math.min(a.y, b.y);
+ a.w = Math.max(a.x+a.w, b.x+b.w) - nx;
+ a.h = Math.max(a.y+a.h, b.y+b.h) - ny;
+ a.x = nx; a.y = ny;
+ return(a);
+};
+
+/**generates a minimally bounding rectangle for all rectangles in
+ * array "nodes". If rect is set, it is modified into the MBR. Otherwise,
+ * a new rectangle is generated and returned.
+ * [ rectangle a ] = make_MBR(rectangle array nodes, rectangle rect)
+ * @static function
+ */
+RTree.Rectangle.make_MBR = function(nodes, rect) {
+ if(nodes.length < 1)
+ return({x:0, y:0, w:0, h:0});
+ //throw "make_MBR: nodes must contain at least one rectangle!";
+ if(!rect)
+ rect = {x:nodes[0].x, y:nodes[0].y, w:nodes[0].w, h:nodes[0].h};
+ else
+ rect.x = nodes[0].x; rect.y = nodes[0].y; rect.w = nodes[0].w; rect.h = nodes[0].h;
+
+ for(var i = nodes.length-1; i>0; i--)
+ RTree.Rectangle.expand_rectangle(rect, nodes[i]);
+
+ return(rect);
+};
diff --git a/test/index.html b/test/index.html
index e7e6701a6..b270dc49b 100644
--- a/test/index.html
+++ b/test/index.html
@@ -14,6 +14,7 @@
+
@@ -52,6 +53,7 @@
+
diff --git a/test/spec/util.js b/test/spec/util.js
index c2c645673..9d5cf082b 100644
--- a/test/spec/util.js
+++ b/test/spec/util.js
@@ -112,5 +112,12 @@ describe('iD.Util', function() {
expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false;
});
});
+
+ describe('#pathLength', function() {
+ it('calculates a simple path length', function() {
+ var path = [[0, 0], [0, 1], [3, 5]];
+ expect(iD.geo.pathLength(path)).to.eql(6);
+ });
+ });
});
});