From 9c007e24e32d5b927970396307220d9949313922 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 17 Jan 2013 15:14:22 -0500 Subject: [PATCH 01/25] Start work on label rendering --- index.html | 1 + js/id/renderer/map.js | 4 ++- js/id/svg/labels.js | 57 +++++++++++++++++++++++++++++++++++++++++++ js/id/svg/surface.js | 2 +- 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 js/id/svg/labels.js diff --git a/index.html b/index.html index b106c35b7..4c46c9c84 100644 --- a/index.html +++ b/index.html @@ -44,6 +44,7 @@ + diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index c0defee3f..99ad5a438 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -21,6 +21,7 @@ iD.Map = function() { lines = iD.svg.Lines(), areas = iD.svg.Areas(), midpoints = iD.svg.Midpoints(), + labels = iD.svg.Labels(), surface, tilegroup; function map(selection) { @@ -96,7 +97,8 @@ iD.Map = function() { .call(vertices, graph, all, filter, projection) .call(lines, graph, all, filter, projection) .call(areas, graph, all, filter, projection) - .call(midpoints, graph, all, filter, projection); + .call(midpoints, graph, all, filter, projection) + .call(labels, graph, all, filter, projection); } function editOff() { diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js new file mode 100644 index 000000000..8ba1549e1 --- /dev/null +++ b/js/id/svg/labels.js @@ -0,0 +1,57 @@ +iD.svg.Labels = function() { + + function drawTexts(group, labels, filter, classes, position) { + var texts = group.selectAll('text') + .filter(filter) + .data(labels); + + texts.enter() + .append('text') + .attr('class', classes); + + texts.attr('x', position('x')) + .attr('y', position('y')) + .attr('transform', position('transform')) + .text(function(d) { return d.tags.name }); + + texts.exit().remove(); + return texts; + } + + return function drawLabels(surface, graph, entities, filter, projection) { + + var points = []; + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + if (entity.geometry() === 'point' && entity.tags.name) { + points.push(entity); + } + } + + var labels = points; + var positions = []; + + var project = iD.svg.RoundProjection(projection); + + for (var i = 0; i < labels.length; i ++) { + positions[i] = {}; + var l = labels[i]; + if (l.type === 'node') { + var coord = project(l.loc); + positions[i].x = coord[0]; + positions[i].y = coord[1]; + } + } + + function position(attr) { + return function(d, i) { return positions[i][attr] }; + } + + var label = surface.select('.layer-label'), + texts = drawTexts(label, labels, filter, 'todo', position); + + + }; + +}; diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index 9fb64ca0e..0da1cc4ac 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -11,7 +11,7 @@ iD.svg.Surface = function() { .attr('clip-path', 'url(#clip)'); var layers = clip.selectAll('.layer') - .data(['fill', 'casing', 'stroke', 'text', 'hit']); + .data(['fill', 'casing', 'stroke', 'text', 'hit', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); From 8a84dd4c8e2820b3d039185ad2ed707994f4ba96 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 17 Jan 2013 16:10:01 -0500 Subject: [PATCH 02/25] start label styles and positioning --- css/map.css | 7 +++++++ js/id/svg/labels.js | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/css/map.css b/css/map.css index b2929f149..af1ea9226 100644 --- a/css/map.css +++ b/css/map.css @@ -284,6 +284,13 @@ text.tag-oneway { pointer-events:none; } +text.label { + font-size: 12px; + font-weight: bold; + fill: black; + text-shadow: 0 0 0.4em white, 1px 1px white, -1px -1px white, -1px 1px white, 1px -1px white; +} + /* Cursors */ #map:hover { diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 8ba1549e1..cf1d484d8 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -1,5 +1,10 @@ iD.svg.Labels = function() { + var pointOffsets = [ + [15, 3, 'start'], // right + [-15, 3, 'end'], // left + ]; + function drawTexts(group, labels, filter, classes, position) { var texts = group.selectAll('text') .filter(filter) @@ -12,6 +17,7 @@ iD.svg.Labels = function() { texts.attr('x', position('x')) .attr('y', position('y')) .attr('transform', position('transform')) + .style('text-anchor', position('textAnchor')) .text(function(d) { return d.tags.name }); texts.exit().remove(); @@ -38,9 +44,11 @@ iD.svg.Labels = function() { positions[i] = {}; var l = labels[i]; if (l.type === 'node') { - var coord = project(l.loc); - positions[i].x = coord[0]; - positions[i].y = coord[1]; + var coord = project(l.loc), + offset = pointOffsets[1]; + positions[i].x = coord[0] + offset[0]; + positions[i].y = coord[1] + offset[1]; + positions[i].textAnchor = offset[2]; } } @@ -49,7 +57,7 @@ iD.svg.Labels = function() { } var label = surface.select('.layer-label'), - texts = drawTexts(label, labels, filter, 'todo', position); + texts = drawTexts(label, labels, filter, 'label', position); }; From 9e784b6bc958fe7ffa2f968954e51163a8ed8718 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Fri, 18 Jan 2013 16:34:41 -0500 Subject: [PATCH 03/25] mostly working path and point labelling --- css/map.css | 6 +- js/id/svg/labels.js | 191 ++++++++++++++++++++++++++++++++++++++------ js/id/svg/lines.js | 9 ++- 3 files changed, 179 insertions(+), 27 deletions(-) diff --git a/css/map.css b/css/map.css index af1ea9226..9d09b1b63 100644 --- a/css/map.css +++ b/css/map.css @@ -284,7 +284,11 @@ text.tag-oneway { pointer-events:none; } -text.label { +text.textpath-label { + text-anchor: middle; +} + +text.textpath-label, text.text-label { font-size: 12px; font-weight: bold; fill: black; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index cf1d484d8..317c0648e 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -1,12 +1,47 @@ iD.svg.Labels = function() { var pointOffsets = [ - [15, 3, 'start'], // right + [25, 3, 'start'], // right [-15, 3, 'end'], // left ]; + var height = 12, + width = 6; + + function drawTextPaths(group, labels, filter, classes, t) { + + var reverse = t('reverse'); + + var texts = group.selectAll('text.textpath-label') + .data(labels); + + var tp = texts.enter() + .append('text') + .attr({ 'class': classes}) + .append('textPath') + .attr({ + 'class': 'textpath', + 'startOffset': '50%' + }); + + var tps = group.selectAll('.textpath-label .textpath') + .data(labels) + .attr({ + 'xlink:href': function(d, i) { return '#casing-' + d.id}, + 'glyph-orientation-vertical': function(d, i) {return reverse(d, i) ? 180 : 0}, + 'glyph-orientation-horizontal': function(d, i) {return reverse(d, i) ? 180 : 0}, + 'dominant-baseline': 'central' + }) + .text(function(d, i) { + return reverse(d, i) ? d.tags.name.split('').reverse().join('') : d.tags.name; + }); + + texts.exit().remove(); + + } + function drawTexts(group, labels, filter, classes, position) { - var texts = group.selectAll('text') + var texts = group.selectAll('text.text-label') .filter(filter) .data(labels); @@ -24,42 +59,152 @@ iD.svg.Labels = function() { return texts; } + function getPathTransform(projection) { + var nodeCache = {}; + return function pathTransform(entity) { + var length = 0, + w = entity.tags.name.length * width; + + var nodes = nodeCache[entity.id]; + if (typeof nodes === 'undefined') { + nodes = nodeCache[entity.id] = _.pluck(entity.nodes, 'loc') + .map(iD.svg.RoundProjection(projection)); + } + + 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); + } + + for (var i = 0; i < nodes.length - 1; i++) { + length += segmentLength(i); + } + + if (length < w + 20) return null; + + var ends = (length - w) / 2, + sofar = 0, + start, end, + n1, n2; + + for (var i = 0; i < nodes.length - 1; i++) { + var current = segmentLength(i); + if (!start && sofar + current > ends) { + var portion = (ends - 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]) + ]; + n1 = nodes[i + 1]; + } + if (!end && sofar + current > length - ends) { + var portion = (length - ends - 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]) + ]; + n2 = nodes[i]; + } + sofar += current; + } + + var angle = Math.atan2(n1[1] - start[1], n1[0] - start[0]); + var reverse = !(start[0] < end[0] && angle < Math.PI/2 && angle > - Math.PI/2); + + return { + length: length, + width: Math.abs(start[0] - end[0]), + height: Math.abs(start[1] - end[1]), + start: start, + end: end, + reverse: reverse + }; + } + } + + return function drawLabels(surface, graph, entities, filter, projection) { - var points = []; + var project = iD.svg.RoundProjection(projection); + var rtree = new RTree(); + + function addPoint(d, i) { + var bbox = this.getBBox(); + var coords = project(d.loc); + var tree = new RTree.Rectangle(coords[0], coords[1], bbox.width, bbox.height); + rtree.insert(tree, d.id); + } + + //d3.selectAll('.node.point').each(addPoint); + + var points = [], + roads = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; - if (entity.geometry() === 'point' && entity.tags.name) { + if (entity.geometry() === 'line' && entity.tags.highway && entity.tags.name) { + roads.push(entity); + } else if (entity.geometry() === 'point' && entity.tags.name) { points.push(entity); } } - var labels = points; - var positions = []; + var entities = roads.concat(points); + var rect; + var pathTransform = getPathTransform(projection); + var textlabels = [], + pathlabels = [], + textpositions = [], + pathpositions = []; - var project = iD.svg.RoundProjection(projection); - - for (var i = 0; i < labels.length; i ++) { - positions[i] = {}; - var l = labels[i]; - if (l.type === 'node') { - var coord = project(l.loc), - offset = pointOffsets[1]; - positions[i].x = coord[0] + offset[0]; - positions[i].y = coord[1] + offset[1]; - positions[i].textAnchor = offset[2]; - } + function nocollisions(rect) { + var v = rtree.search(rect, true).length === 0; + if (v) rtree.insert(rect); + return v; } - function position(attr) { - return function(d, i) { return positions[i][attr] }; + for (var i = 0; i < entities.length; i ++) { + var p = {}, + entity = entities[i], + w = width * entity.tags.name.length, + h = 20; + + if (entity.type === 'node') { + var coord = project(entity.loc), + offset = pointOffsets[0]; + p.x = coord[0] + offset[0]; + p.y = coord[1] + offset[1]; + p.textAnchor = offset[2]; + rect = new RTree.Rectangle(p.x, p.y, width * entity.tags.name.length, 20); + if (nocollisions(rect)) { + textpositions.push(p); + textlabels.push(entity); + } + + } else if (entity.type === 'way' && entity.geometry() === 'line') { + p = pathTransform(entity); + if (!p) continue; + rect = new RTree.Rectangle(Math.min(p.start[0], p.end[0]) - 5, Math.min(p.start[1], p.end[1]) - 5, p.width + 10, p.height + 10); + if (nocollisions(rect)) { + pathpositions.push(p); + pathlabels.push(entity); + } + } + + } + + + function textposition(attr) { + return function(d, i) { return textpositions[i][attr] }; + } + function pathposition(attr) { + return function(d, i) { return pathpositions[i][attr] }; } var label = surface.select('.layer-label'), - texts = drawTexts(label, labels, filter, 'label', position); - - + texts = drawTexts(label, textlabels, filter, 'text-label', textposition), + textPaths = drawTextPaths(label, pathlabels, filter, 'textpath-label', pathposition); }; }; diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 3f1b634d7..48eef2a26 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -33,13 +33,16 @@ iD.svg.Lines = function() { return as - bs; } - function drawPaths(group, lines, filter, classes, lineString) { + function drawPaths(group, lines, filter, classes, lineString, prefix) { var paths = group.selectAll('path') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') + .attr('id', function(d) { + return prefix + d.id; + }) .attr('class', classes); paths @@ -87,8 +90,8 @@ iD.svg.Lines = function() { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); + casings = drawPaths(casing, lines, filter, 'way line casing', lineString, 'casing-'), + strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString, 'stroke-'); // Determine the lengths of oneway paths var lengths = {}, From d70b5c389197f31ae5bb30765a300eaad97cf7b1 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Fri, 18 Jan 2013 17:01:41 -0500 Subject: [PATCH 04/25] Add rtree to index.html --- index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/index.html b/index.html index 4c46c9c84..9938836eb 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,7 @@ + From 572c111bf118f3e0b25f63fbb324ae6e35552e73 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Sun, 20 Jan 2013 22:21:18 -0500 Subject: [PATCH 05/25] Add polygonCentroid and polygon area to utils --- js/id/util.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/js/id/util.js b/js/id/util.js index 79b65c524..9feb4ee38 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -145,3 +145,27 @@ iD.util.geo.polygonIntersectsPolygon = function(outer, inner) { return iD.util.geo.pointInPolygon(point, outer); }); }; + +// May have issues with self interesecting polygons +iD.util.geo.polygonCentroid = function(polygon) { + var x = 0, + y = 0, + area = iD.util.geo.area(polygon); + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + x += (xi + xj) * (xj * yi - xi * yj); + y += (yi + yj) * (xj * yi - xi * yj); + } + return [x / 6 / area, y / 6 / area]; +}; + +iD.util.geo.area = function(polygon) { + var area = 0; + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + area += xj * yi - xi * yj; + } + return area/2; +}; From 71f6402df03de65128d72b4d6971e0f859424342 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Sun, 20 Jan 2013 22:46:30 -0500 Subject: [PATCH 06/25] add very basic support for area labels --- js/id/svg/labels.js | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 317c0648e..1333278ab 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -1,4 +1,4 @@ -iD.svg.Labels = function() { +iD.svg.Labels = function(projection) { var pointOffsets = [ [25, 3, 'start'], // right @@ -124,14 +124,13 @@ iD.svg.Labels = function() { } - return function drawLabels(surface, graph, entities, filter, projection) { + return function drawLabels(surface, graph, entities, filter) { - var project = iD.svg.RoundProjection(projection); var rtree = new RTree(); function addPoint(d, i) { var bbox = this.getBBox(); - var coords = project(d.loc); + var coords = projection(d.loc); var tree = new RTree.Rectangle(coords[0], coords[1], bbox.width, bbox.height); rtree.insert(tree, d.id); } @@ -139,7 +138,8 @@ iD.svg.Labels = function() { //d3.selectAll('.node.point').each(addPoint); var points = [], - roads = []; + roads = [], + buildings = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -147,10 +147,12 @@ iD.svg.Labels = function() { roads.push(entity); } else if (entity.geometry() === 'point' && entity.tags.name) { points.push(entity); + } else if (entity.geometry() === 'area' && entity.tags.building && entity.tags.name) { + buildings.push(entity); } } - var entities = roads.concat(points); + var entities = roads.concat(buildings).concat(points); var rect; var pathTransform = getPathTransform(projection); var textlabels = [], @@ -171,12 +173,12 @@ iD.svg.Labels = function() { h = 20; if (entity.type === 'node') { - var coord = project(entity.loc), + var coord = projection(entity.loc), offset = pointOffsets[0]; p.x = coord[0] + offset[0]; p.y = coord[1] + offset[1]; p.textAnchor = offset[2]; - rect = new RTree.Rectangle(p.x, p.y, width * entity.tags.name.length, 20); + rect = new RTree.Rectangle(p.x, p.y, w, 20); if (nocollisions(rect)) { textpositions.push(p); textlabels.push(entity); @@ -190,6 +192,25 @@ iD.svg.Labels = function() { pathpositions.push(p); pathlabels.push(entity); } + + } else if (entity.type === 'way' && entity.geometry() === 'area') { + var nodes = _.pluck(entity.nodes, 'loc') + .map(iD.svg.RoundProjection(projection)), + centroid = iD.util.geo.polygonCentroid(nodes), + extent = entity.extent(graph), + entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; + + if (entitywidth < w + 20) { + continue; + } + p.x = centroid[0]; + p.y = centroid[1]; + p.textAnchor = 'middle'; + rect = new RTree.Rectangle(p.x - w/2, p.y, w, 20); + if (nocollisions(rect)) { + textpositions.push(p); + textlabels.push(entity); + } } } From 9aa74c404d531a0b9cef4e48eecf9e5bb924e438 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 11:01:46 -0500 Subject: [PATCH 07/25] Add rtree.js --- js/lib/rtree.js | 711 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 711 insertions(+) create mode 100644 js/lib/rtree.js 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); +}; From 397372b5055ff45fbd71150913b5f6feeaf29d5d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 11:04:39 -0500 Subject: [PATCH 08/25] start rearranging label code --- js/id/svg/labels.js | 164 +++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 1333278ab..0039ee18d 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -8,12 +8,13 @@ iD.svg.Labels = function(projection) { var height = 12, width = 6; - function drawTextPaths(group, labels, filter, classes, t) { + function drawLineLabels(group, labels, filter, classes, position) { - var reverse = t('reverse'); + var reverse = position('reverse'); - var texts = group.selectAll('text.textpath-label') - .data(labels); + var texts = group.selectAll('text.' + classes) + .filter(filter) + .data(labels, iD.Entity.key); var tp = texts.enter() .append('text') @@ -40,10 +41,10 @@ iD.svg.Labels = function(projection) { } - function drawTexts(group, labels, filter, classes, position) { - var texts = group.selectAll('text.text-label') + function drawPointLabels(group, labels, filter, classes, position) { + var texts = group.selectAll('text.' + classes) .filter(filter) - .data(labels); + .data(labels, iD.Entity.key); texts.enter() .append('text') @@ -128,15 +129,6 @@ iD.svg.Labels = function(projection) { var rtree = new RTree(); - function addPoint(d, i) { - var bbox = this.getBBox(); - var coords = projection(d.loc); - var tree = new RTree.Rectangle(coords[0], coords[1], bbox.width, bbox.height); - rtree.insert(tree, d.id); - } - - //d3.selectAll('.node.point').each(addPoint); - var points = [], roads = [], buildings = []; @@ -153,79 +145,95 @@ iD.svg.Labels = function(projection) { } var entities = roads.concat(buildings).concat(points); - var rect; var pathTransform = getPathTransform(projection); - var textlabels = [], - pathlabels = [], - textpositions = [], - pathpositions = []; - function nocollisions(rect) { + var positions = { + point: [], + line: [], + area: [] + }; + + var labelled = { + point: [], + line: [], + area: [] + }; + + + for (var i = 0; i < entities.length; i ++) { + var entity = entities[i], + p; + if (entity.geometry() === 'point') { + p = getPointLabel(entity); + } else if (entity.geometry() === 'line') { + p = getLineLabel(entity); + } else if (entity.geometry() === 'area') { + p = getAreaLabel(entity); + } + if (p) { + positions[entity.geometry()].push(p); + labelled[entity.geometry()].push(entity); + } + } + + function getPointLabel(entity) { + var coord = projection(entity.loc), + offset = pointOffsets[0], + p = {}, + w = width * entity.tags.name.length; + p.x = coord[0] + offset[0]; + p.y = coord[1] + offset[1]; + p.textAnchor = offset[2]; + var rect = new RTree.Rectangle(p.x, p.y, w, 20); + if (tryInsert(rect)) return p; + } + + function getLineLabel(entity) { + var p = pathTransform(entity); + if (!p) return; + var rect = new RTree.Rectangle(Math.min(p.start[0], p.end[0]) - 5, Math.min(p.start[1], p.end[1]) - 5, p.width + 10, p.height + 10); + if (tryInsert(rect)) return p; + } + + function getAreaLabel(entity) { + var nodes = _.pluck(entity.nodes, 'loc') + .map(iD.svg.RoundProjection(projection)), + centroid = iD.util.geo.polygonCentroid(nodes), + extent = entity.extent(graph), + entitywidth = projection(extent[1])[0] - projection(extent[0])[0], + w = width * entity.tags.name.length; + p = {}; + + if (entitywidth < w + 20) return; + p.x = centroid[0]; + p.y = centroid[1]; + p.textAnchor = 'middle'; + var rect = new RTree.Rectangle(p.x - w/2, p.y, w, 20); + if (tryInsert(rect)) return p; + + } + + function tryInsert(rect) { var v = rtree.search(rect, true).length === 0; if (v) rtree.insert(rect); return v; } - for (var i = 0; i < entities.length; i ++) { - var p = {}, - entity = entities[i], - w = width * entity.tags.name.length, - h = 20; - - if (entity.type === 'node') { - var coord = projection(entity.loc), - offset = pointOffsets[0]; - p.x = coord[0] + offset[0]; - p.y = coord[1] + offset[1]; - p.textAnchor = offset[2]; - rect = new RTree.Rectangle(p.x, p.y, w, 20); - if (nocollisions(rect)) { - textpositions.push(p); - textlabels.push(entity); - } - - } else if (entity.type === 'way' && entity.geometry() === 'line') { - p = pathTransform(entity); - if (!p) continue; - rect = new RTree.Rectangle(Math.min(p.start[0], p.end[0]) - 5, Math.min(p.start[1], p.end[1]) - 5, p.width + 10, p.height + 10); - if (nocollisions(rect)) { - pathpositions.push(p); - pathlabels.push(entity); - } - - } else if (entity.type === 'way' && entity.geometry() === 'area') { - var nodes = _.pluck(entity.nodes, 'loc') - .map(iD.svg.RoundProjection(projection)), - centroid = iD.util.geo.polygonCentroid(nodes), - extent = entity.extent(graph), - entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; - - if (entitywidth < w + 20) { - continue; - } - p.x = centroid[0]; - p.y = centroid[1]; - p.textAnchor = 'middle'; - rect = new RTree.Rectangle(p.x - w/2, p.y, w, 20); - if (nocollisions(rect)) { - textpositions.push(p); - textlabels.push(entity); - } - } - + function pointposition(attr) { + return function(d, i) { return positions['point'][i][attr] }; + } + function lineposition(attr) { + return function(d, i) { return positions['line'][i][attr] }; + } + function areaposition(attr) { + return function(d, i) { return positions['area'][i][attr] }; } - function textposition(attr) { - return function(d, i) { return textpositions[i][attr] }; - } - function pathposition(attr) { - return function(d, i) { return pathpositions[i][attr] }; - } - var label = surface.select('.layer-label'), - texts = drawTexts(label, textlabels, filter, 'text-label', textposition), - textPaths = drawTextPaths(label, pathlabels, filter, 'textpath-label', pathposition); + points = drawPointLabels(label, labelled['point'], filter, 'text-label', pointposition), + lines = drawLineLabels(label, labelled['line'], filter, 'textpath-label', lineposition), + areas = drawPointLabels(label, labelled['area'], filter, 'text-arealabel', areaposition); }; }; From 00e5ca25eed9c9554ff5b70b213c36523237c5e9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 12:03:23 -0500 Subject: [PATCH 09/25] Split up pathTransform --- js/id/svg/labels.js | 121 ++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 65 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 0039ee18d..f7a5d2bd4 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -60,68 +60,47 @@ iD.svg.Labels = function(projection) { return texts; } - function getPathTransform(projection) { - var nodeCache = {}; - return function pathTransform(entity) { - var length = 0, - w = entity.tags.name.length * width; + 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; + } - var nodes = nodeCache[entity.id]; - if (typeof nodes === 'undefined') { - nodes = nodeCache[entity.id] = _.pluck(entity.nodes, 'loc') - .map(iD.svg.RoundProjection(projection)); - } - - 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); - } - - for (var i = 0; i < nodes.length - 1; i++) { - length += segmentLength(i); - } - - if (length < w + 20) return null; - - var ends = (length - w) / 2, - sofar = 0, - start, end, - n1, n2; - - for (var i = 0; i < nodes.length - 1; i++) { - var current = segmentLength(i); - if (!start && sofar + current > ends) { - var portion = (ends - 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]) - ]; - n1 = nodes[i + 1]; - } - if (!end && sofar + current > length - ends) { - var portion = (length - ends - 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]) - ]; - n2 = nodes[i]; - } - sofar += current; - } - - var angle = Math.atan2(n1[1] - start[1], n1[0] - start[0]); - var reverse = !(start[0] < end[0] && angle < Math.PI/2 && angle > - Math.PI/2); - - return { - length: length, - width: Math.abs(start[0] - end[0]), - height: Math.abs(start[1] - end[1]), - start: start, - end: end, - reverse: reverse - }; + 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; + } + sofar += current; + + } + var ret = nodes.slice(i0, i1); + ret.unshift(start); + ret.push(end); + return ret; + } @@ -145,7 +124,6 @@ iD.svg.Labels = function(projection) { } var entities = roads.concat(buildings).concat(points); - var pathTransform = getPathTransform(projection); var positions = { point: [], @@ -189,10 +167,23 @@ iD.svg.Labels = function(projection) { } function getLineLabel(entity) { - var p = pathTransform(entity); - if (!p) return; - var rect = new RTree.Rectangle(Math.min(p.start[0], p.end[0]) - 5, Math.min(p.start[1], p.end[1]) - 5, p.width + 10, p.height + 10); - if (tryInsert(rect)) return p; + var nodes = _.pluck(entity.nodes, 'loc').map(projection), + length = iD.util.geo.pathLength(nodes), + w = width * entity.tags.name.length; + if (length < w + 20) return; + + var ends = (length - w) / 2, + sub = subpath(nodes, ends, length - ends), + rev = reverse(sub), + rect = new RTree.Rectangle( + Math.min(sub[0][0], sub[sub.length - 1][0]) - 5, + Math.min(sub[0][1], sub[sub.length - 1][1]) - 5, + Math.abs(sub[0][0] - sub[sub.length - 1][0]) + 10, + Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 10 + ); + if (tryInsert(rect)) return { + reverse: rev + }; } function getAreaLabel(entity) { From 079a45650039291ab4996cfddca9ea7557e13957 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 12:35:15 -0500 Subject: [PATCH 10/25] Add pathLength to utils --- js/id/util.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/js/id/util.js b/js/id/util.js index 9feb4ee38..def4e36d6 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -169,3 +169,14 @@ iD.util.geo.area = function(polygon) { } return area/2; }; + +iD.util.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; +}; From 14272ef20042b9eca30f15b87fda18b6e582b074 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 13:03:53 -0500 Subject: [PATCH 11/25] Add label_stack --- js/id/svg/labels.js | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index f7a5d2bd4..8e6cd2baa 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -1,5 +1,13 @@ 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', ''] + ]; + var pointOffsets = [ [25, 3, 'start'], // right [-15, 3, 'end'], // left @@ -108,22 +116,21 @@ iD.svg.Labels = function(projection) { var rtree = new RTree(); - var points = [], - roads = [], - buildings = []; + var labelable = []; + for (var i = 0; i < label_stack.length; i++) labelable.push([]); for (var i = 0; i < entities.length; i++) { var entity = entities[i]; - if (entity.geometry() === 'line' && entity.tags.highway && entity.tags.name) { - roads.push(entity); - } else if (entity.geometry() === 'point' && entity.tags.name) { - points.push(entity); - } else if (entity.geometry() === 'area' && entity.tags.building && entity.tags.name) { - buildings.push(entity); + if (!entity.tags.name) 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 entities = roads.concat(buildings).concat(points); var positions = { point: [], @@ -137,9 +144,10 @@ iD.svg.Labels = function(projection) { area: [] }; + labelable = _.flatten(labelable); - for (var i = 0; i < entities.length; i ++) { - var entity = entities[i], + for (var i = 0; i < labelable.length; i ++) { + var entity = labelable[i], p; if (entity.geometry() === 'point') { p = getPointLabel(entity); From 0249d6b3eb0965f817f8fcce525c2ad68061e05c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 14:04:19 -0500 Subject: [PATCH 12/25] Try multiple line label positions --- js/id/svg/labels.js | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 8e6cd2baa..b4b1b8e9a 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -13,6 +13,10 @@ iD.svg.Labels = function(projection) { [-15, 3, 'end'], // left ]; + var lineOffsets = [ + 50, 40, 60, 30, 70 + ] + var height = 12, width = 6; @@ -22,20 +26,21 @@ iD.svg.Labels = function(projection) { var texts = group.selectAll('text.' + classes) .filter(filter) - .data(labels, iD.Entity.key); + .data(labels, iD.Entity.key) var tp = texts.enter() .append('text') .attr({ 'class': classes}) .append('textPath') .attr({ - 'class': 'textpath', - 'startOffset': '50%' + 'class': 'textpath' }); + var tps = group.selectAll('.textpath-label .textpath') .data(labels) .attr({ + 'startOffset': position('startOffset'), 'xlink:href': function(d, i) { return '#casing-' + d.id}, 'glyph-orientation-vertical': function(d, i) {return reverse(d, i) ? 180 : 0}, 'glyph-orientation-horizontal': function(d, i) {return reverse(d, i) ? 180 : 0}, @@ -180,18 +185,25 @@ iD.svg.Labels = function(projection) { w = width * entity.tags.name.length; if (length < w + 20) return; - var ends = (length - w) / 2, - sub = subpath(nodes, ends, length - ends), - rev = reverse(sub), - rect = new RTree.Rectangle( - Math.min(sub[0][0], sub[sub.length - 1][0]) - 5, - Math.min(sub[0][1], sub[sub.length - 1][1]) - 5, - Math.abs(sub[0][0] - sub[sub.length - 1][0]) + 10, - Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 10 - ); - if (tryInsert(rect)) return { - reverse: rev - }; + // 50, 40, 60, 30, 70 + for (var i = 0; i < 5; i ++) { + var offset = lineOffsets[i], + middle = offset / 100 * length; + if (middle < w / 2) return; + var start = middle - w/2, + sub = subpath(nodes, start, start + w), + 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]) + 20 + ); + if (tryInsert(rect)) return { + reverse: rev, + startOffset: offset + '%' + } + } } function getAreaLabel(entity) { From 2b7aa9309601ad947e382155ffbb715cf271e66b Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 14:47:50 -0500 Subject: [PATCH 13/25] Add classes to labels for styling --- css/map.css | 10 ++++++++- js/id/svg/labels.js | 50 ++++++++++++++++++++++++--------------------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/css/map.css b/css/map.css index 833ff39b6..608c63ef0 100644 --- a/css/map.css +++ b/css/map.css @@ -557,13 +557,21 @@ text.textpath-label { text-anchor: middle; } -text.textpath-label, text.text-label { +.layer-label text{ font-size: 12px; font-weight: bold; fill: black; text-shadow: 0 0 0.4em white, 1px 1px white, -1px -1px white, -1px 1px white, 1px -1px white; } +text.area-leisure-park { + font-size: 14px; +} + +text.point-amenity{ + font-size: 9px; +} + /* Cursors */ #map:hover { diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index b4b1b8e9a..546e3282b 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -2,15 +2,16 @@ iD.svg.Labels = function(projection) { // Replace with dict and iterate over entities tags instead? var label_stack = [ - ['line', 'highway', ''], + ['line', 'highway'], ['area', 'building', 'yes'], ['area', 'leisure', 'park'], - ['area', 'natural', ''] + ['area', 'natural'], + ['point', 'amenity'] ]; var pointOffsets = [ - [25, 3, 'start'], // right - [-15, 3, 'end'], // left + [15, 0, 'start'], // right + [-15, 0, 'end'], // left ]; var lineOffsets = [ @@ -22,7 +23,8 @@ iD.svg.Labels = function(projection) { function drawLineLabels(group, labels, filter, classes, position) { - var reverse = position('reverse'); + var reverse = position('reverse'), + getClasses = position('classes'); var texts = group.selectAll('text.' + classes) .filter(filter) @@ -30,7 +32,7 @@ iD.svg.Labels = function(projection) { var tp = texts.enter() .append('text') - .attr({ 'class': classes}) + .attr({ 'class': function(d, i) { return classes + ' ' + getClasses(d, i);}}) .append('textPath') .attr({ 'class': 'textpath' @@ -55,13 +57,14 @@ iD.svg.Labels = function(projection) { } function drawPointLabels(group, labels, filter, classes, position) { + var getClasses = position('classes'); var texts = group.selectAll('text.' + classes) .filter(filter) .data(labels, iD.Entity.key); texts.enter() .append('text') - .attr('class', classes); + .attr('class', function(d, i) { return classes + ' ' + getClasses(d, i);}); texts.attr('x', position('x')) .attr('y', position('y')) @@ -149,21 +152,22 @@ iD.svg.Labels = function(projection) { area: [] }; - labelable = _.flatten(labelable); - - for (var i = 0; i < labelable.length; i ++) { - var entity = labelable[i], - p; - if (entity.geometry() === 'point') { - p = getPointLabel(entity); - } else if (entity.geometry() === 'line') { - p = getLineLabel(entity); - } else if (entity.geometry() === 'area') { - p = getAreaLabel(entity); - } - if (p) { - positions[entity.geometry()].push(p); - labelled[entity.geometry()].push(entity); + for (var k = 0; k < labelable.length; k++) { + for (var i = 0; i < labelable[k].length; i ++) { + var entity = labelable[k][i], + p; + if (entity.geometry() === 'point') { + p = getPointLabel(entity); + } else if (entity.geometry() === 'line') { + p = getLineLabel(entity); + } else if (entity.geometry() === 'area') { + p = getAreaLabel(entity); + } + if (p) { + p.classes = label_stack[k].join('-'); + positions[entity.geometry()].push(p); + labelled[entity.geometry()].push(entity); + } } } @@ -197,7 +201,7 @@ iD.svg.Labels = function(projection) { 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]) + 20 + Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30 ); if (tryInsert(rect)) return { reverse: rev, From daff9c5a2f7fcb1bd5eee434a7d736a3a37ad478 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 22 Jan 2013 17:40:05 -0500 Subject: [PATCH 14/25] Labels height pulled from font-size css --- js/id/svg/labels.js | 38 +++++++++++++++++++++++++------------- js/id/util.js | 12 ++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 546e3282b..98094f591 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -6,9 +6,18 @@ iD.svg.Labels = function(projection) { ['area', 'building', 'yes'], ['area', 'leisure', 'park'], ['area', 'natural'], - ['point', 'amenity'] + ['point', 'amenity'], + ['point', 'shop'], ]; + var default_size = 12; + var font_sizes = label_stack.map(function(d) { + var style = iD.util.getStyle('text.' + d.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, 0, 'start'], // right [-15, 0, 'end'], // left @@ -16,7 +25,7 @@ iD.svg.Labels = function(projection) { var lineOffsets = [ 50, 40, 60, 30, 70 - ] + ]; var height = 12, width = 6; @@ -46,7 +55,7 @@ iD.svg.Labels = function(projection) { 'xlink:href': function(d, i) { return '#casing-' + d.id}, 'glyph-orientation-vertical': function(d, i) {return reverse(d, i) ? 180 : 0}, 'glyph-orientation-horizontal': function(d, i) {return reverse(d, i) ? 180 : 0}, - 'dominant-baseline': 'central' + 'dominant-baseline': 'middle' }) .text(function(d, i) { return reverse(d, i) ? d.tags.name.split('').reverse().join('') : d.tags.name; @@ -123,6 +132,7 @@ iD.svg.Labels = function(projection) { return function drawLabels(surface, graph, entities, filter) { var rtree = new RTree(); + var hidePoints = !d3.select('.point').node(); var labelable = []; for (var i = 0; i < label_stack.length; i++) labelable.push([]); @@ -130,6 +140,7 @@ iD.svg.Labels = function(projection) { 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]]) { @@ -153,15 +164,16 @@ iD.svg.Labels = function(projection) { }; 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], p; if (entity.geometry() === 'point') { - p = getPointLabel(entity); + p = getPointLabel(entity, font_size); } else if (entity.geometry() === 'line') { - p = getLineLabel(entity); + p = getLineLabel(entity, font_size); } else if (entity.geometry() === 'area') { - p = getAreaLabel(entity); + p = getAreaLabel(entity, font_size); } if (p) { p.classes = label_stack[k].join('-'); @@ -171,11 +183,11 @@ iD.svg.Labels = function(projection) { } } - function getPointLabel(entity) { + function getPointLabel(entity, font) { var coord = projection(entity.loc), offset = pointOffsets[0], p = {}, - w = width * entity.tags.name.length; + w = font / 2 * entity.tags.name.length; p.x = coord[0] + offset[0]; p.y = coord[1] + offset[1]; p.textAnchor = offset[2]; @@ -183,17 +195,17 @@ iD.svg.Labels = function(projection) { if (tryInsert(rect)) return p; } - function getLineLabel(entity) { + function getLineLabel(entity, font) { var nodes = _.pluck(entity.nodes, 'loc').map(projection), length = iD.util.geo.pathLength(nodes), - w = width * entity.tags.name.length; + w = font / 2 * entity.tags.name.length; if (length < w + 20) return; // 50, 40, 60, 30, 70 for (var i = 0; i < 5; i ++) { var offset = lineOffsets[i], middle = offset / 100 * length; - if (middle < w / 2) return; + if (middle <= w / 2) return; var start = middle - w/2, sub = subpath(nodes, start, start + w), rev = reverse(sub), @@ -210,13 +222,13 @@ iD.svg.Labels = function(projection) { } } - function getAreaLabel(entity) { + function getAreaLabel(entity, font) { var nodes = _.pluck(entity.nodes, 'loc') .map(iD.svg.RoundProjection(projection)), centroid = iD.util.geo.polygonCentroid(nodes), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0], - w = width * entity.tags.name.length; + w = font / 2 * entity.tags.name.length; p = {}; if (entitywidth < w + 20) return; diff --git a/js/id/util.js b/js/id/util.js index 51d6fb3f9..b786dd0ba 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -71,6 +71,18 @@ 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]; + } + } + } +}; + iD.util.geo = {}; iD.util.geo.roundCoords = function(c) { From 4a87ae829838ac88d1ef137ce05bbb97b717ff69 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 11:48:04 -0500 Subject: [PATCH 15/25] Point halos and test width calculation/estimation --- css/map.css | 16 +++++-- js/id/svg/labels.js | 105 +++++++++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 34 deletions(-) diff --git a/css/map.css b/css/map.css index a1a73d219..c640fa042 100644 --- a/css/map.css +++ b/css/map.css @@ -565,14 +565,24 @@ text.textpath-label { font-size: 12px; font-weight: bold; fill: black; - text-shadow: 0 0 0.4em white, 1px 1px white, -1px -1px white, -1px 1px white, 1px -1px white; } text.area-leisure-park { - font-size: 14px; + font-size: 16px; } -text.point-amenity{ +.layer-label rect { + opacity: 0.7; + pointer-events: none; +} + +rect.area-building-yes { + opacity: 0.5; +} +text.area-building-yes { +} + +text.point-shop,text.point-amenity{ font-size: 9px; } diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 98094f591..599fe167c 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -19,7 +19,8 @@ iD.svg.Labels = function(projection) { }); var pointOffsets = [ - [15, 0, 'start'], // right + [10, 3, 'start'], // right + [10, 0, 'start'], [-15, 0, 'end'], // left ]; @@ -27,8 +28,14 @@ iD.svg.Labels = function(projection) { 50, 40, 60, 30, 70 ]; - var height = 12, - width = 6; + 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, labels, filter, classes, position) { @@ -65,8 +72,37 @@ iD.svg.Labels = function(projection) { } + function drawPointHalos(group, labels, filter, classes, position) { + + var halos = group.selectAll('rect.' + classes) + .filter(filter) + .data(labels, iD.Entity.key); + + halos.enter() + .insert('rect', '.text-label') + .attr('class', function(d, i) { return classes + ' ' + position('classes')(d, i);}); + + halos.attr({ + 'x': function(d, i) { + var x = position('x')(d, i) - 2; + if (position('textAnchor')(d, i) === 'middle') { + x -= textWidth(d.tags.name, position('height')(d, i)) / 2; + } + return x; + }, + 'y': function(d, i) { return position('y')(d, i) - position('height')(d, i) + 1 - 2; }, + 'width': function(d, i) { return textWidth(d.tags.name, position('height')(d, i)) + 4 }, + 'height': function(d, i) { return position('height')(d, i) + 4 }, + 'fill': 'white', + }); + + halos.exit().remove(); + } + + function drawPointLabels(group, labels, filter, classes, position) { var getClasses = position('classes'); + var texts = group.selectAll('text.' + classes) .filter(filter) .data(labels, iD.Entity.key); @@ -79,7 +115,8 @@ iD.svg.Labels = function(projection) { .attr('y', position('y')) .attr('transform', position('transform')) .style('text-anchor', position('textAnchor')) - .text(function(d) { return d.tags.name }); + .text(function(d) { return d.tags.name }) + .each(function(d, i) { textWidth(d.tags.name, position('height')(d, i), this); }); texts.exit().remove(); return texts; @@ -167,13 +204,14 @@ iD.svg.Labels = function(projection) { 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, font_size); + p = getPointLabel(entity, width, font_size); } else if (entity.geometry() === 'line') { - p = getLineLabel(entity, font_size); + p = getLineLabel(entity, width, font_size); } else if (entity.geometry() === 'area') { - p = getAreaLabel(entity, font_size); + p = getAreaLabel(entity, width, font_size); } if (p) { p.classes = label_stack[k].join('-'); @@ -183,31 +221,32 @@ iD.svg.Labels = function(projection) { } } - function getPointLabel(entity, font) { + function getPointLabel(entity, width, height) { var coord = projection(entity.loc), offset = pointOffsets[0], - p = {}, - w = font / 2 * entity.tags.name.length; - p.x = coord[0] + offset[0]; - p.y = coord[1] + offset[1]; - p.textAnchor = offset[2]; - var rect = new RTree.Rectangle(p.x, p.y, w, 20); + 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, p.y, width, height); if (tryInsert(rect)) return p; } - function getLineLabel(entity, font) { + function getLineLabel(entity, width, height) { var nodes = _.pluck(entity.nodes, 'loc').map(projection), - length = iD.util.geo.pathLength(nodes), - w = font / 2 * entity.tags.name.length; - if (length < w + 20) return; + length = iD.util.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 <= w / 2) return; - var start = middle - w/2, - sub = subpath(nodes, start, start + w), + 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, @@ -216,26 +255,28 @@ iD.svg.Labels = function(projection) { Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30 ); if (tryInsert(rect)) return { + 'font-size': height, reverse: rev, startOffset: offset + '%' } } } - function getAreaLabel(entity, font) { + function getAreaLabel(entity, width, height) { var nodes = _.pluck(entity.nodes, 'loc') .map(iD.svg.RoundProjection(projection)), centroid = iD.util.geo.polygonCentroid(nodes), extent = entity.extent(graph), - entitywidth = projection(extent[1])[0] - projection(extent[0])[0], - w = font / 2 * entity.tags.name.length; - p = {}; + entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; - if (entitywidth < w + 20) return; - p.x = centroid[0]; - p.y = centroid[1]; - p.textAnchor = 'middle'; - var rect = new RTree.Rectangle(p.x - w/2, p.y, w, 20); + 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; } @@ -259,8 +300,10 @@ iD.svg.Labels = function(projection) { var label = surface.select('.layer-label'), points = drawPointLabels(label, labelled['point'], filter, 'text-label', pointposition), + pointHalos = drawPointHalos(label, labelled['point'], filter, 'point-label-halo', pointposition), lines = drawLineLabels(label, labelled['line'], filter, 'textpath-label', lineposition), - areas = drawPointLabels(label, labelled['area'], filter, 'text-arealabel', areaposition); + areas = drawPointLabels(label, labelled['area'], filter, 'text-arealabel', areaposition), + areaHalos = drawPointHalos(label, labelled['area'], filter, 'area-label-halo', areaposition); }; }; From ca9900a91cd0a57546670b692d935ac47dddc1a7 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 12:46:39 -0500 Subject: [PATCH 16/25] Halos and proper orientation for line labels --- js/id/svg/labels.js | 54 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 599fe167c..2144946ca 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -37,7 +37,21 @@ iD.svg.Labels = function(projection) { else return size / 3 * 2 * text.length; } - function drawLineLabels(group, labels, filter, classes, position) { + function drawLineLabels(group, defs, labels, filter, classes, position) { + + var uses = defs.selectAll('path.label') + .filter(filter) + .data(labels, iD.Entity.key); + + uses.enter() + .append('path') + .attr('class', 'label'); + + uses + .attr('id', function(d) { return 'labelshadow-' + d.id; }) + .attr('d', position('lineString')); + + uses.exit().remove(); var reverse = position('reverse'), getClasses = position('classes'); @@ -59,19 +73,37 @@ iD.svg.Labels = function(projection) { .data(labels) .attr({ 'startOffset': position('startOffset'), - 'xlink:href': function(d, i) { return '#casing-' + d.id}, - 'glyph-orientation-vertical': function(d, i) {return reverse(d, i) ? 180 : 0}, - 'glyph-orientation-horizontal': function(d, i) {return reverse(d, i) ? 180 : 0}, + 'xlink:href': function(d, i) { return '#labelshadow-' + d.id}, 'dominant-baseline': 'middle' }) .text(function(d, i) { return reverse(d, i) ? d.tags.name.split('').reverse().join('') : d.tags.name; }); + texts.each(function(d, i) { textWidth(d.tags.name, position('height')(d, i), this); }); + texts.exit().remove(); } + function drawLineHalos(group, labels, filter, classes, position) { + + var halos = group.selectAll('path') + .filter(filter) + .data(labels, iD.Entity.key); + + halos.enter() + .append('path') + .style({ + 'stroke-width': position('font-size') + }) + .attr('class', classes); + + halos.attr('d', position('lineString')); + + halos.exit().remove(); + } + function drawPointHalos(group, labels, filter, classes, position) { var halos = group.selectAll('rect.' + classes) @@ -128,6 +160,10 @@ iD.svg.Labels = function(projection) { 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]; @@ -254,9 +290,10 @@ iD.svg.Labels = function(projection) { 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, - reverse: rev, + 'font-size': height + 2, + lineString: lineString(sub), startOffset: offset + '%' } } @@ -299,9 +336,12 @@ iD.svg.Labels = function(projection) { var label = surface.select('.layer-label'), + halo = surface.select('.layer-halo'), + defs = surface.select('defs'), points = drawPointLabels(label, labelled['point'], filter, 'text-label', pointposition), pointHalos = drawPointHalos(label, labelled['point'], filter, 'point-label-halo', pointposition), - lines = drawLineLabels(label, labelled['line'], filter, 'textpath-label', lineposition), + lines = drawLineLabels(label, defs, labelled['line'], filter, 'textpath-label', lineposition), + linesHalos = drawLineHalos(halo, labelled['line'], filter, 'textpath-label', lineposition), areas = drawPointLabels(label, labelled['area'], filter, 'text-arealabel', areaposition), areaHalos = drawPointHalos(label, labelled['area'], filter, 'area-label-halo', areaposition); }; From b678486c0190d89d43cb705a393f5bb41a21860f Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:08:43 -0500 Subject: [PATCH 17/25] Label bugfixes and cleanup --- css/map.css | 45 ++++++++++------ js/id/svg/labels.js | 123 ++++++++++++++++--------------------------- js/id/svg/surface.js | 2 +- 3 files changed, 77 insertions(+), 93 deletions(-) diff --git a/css/map.css b/css/map.css index c640fa042..594b23b61 100644 --- a/css/map.css +++ b/css/map.css @@ -557,32 +557,47 @@ text.tag-oneway { pointer-events:none; } -text.textpath-label { - text-anchor: middle; +/* + * Labels + */ + +.layer-halo path { + point-events: none; + stroke-linecap: round; + stroke-linejoin: bevel; + stroke-width: 20px; + opacity: 0.8; + stroke: white; } -.layer-label text{ +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, +.area-halo { + opacity: 0.7; + pointer-events: none; +} + + text.area-leisure-park { font-size: 16px; } -.layer-label rect { - opacity: 0.7; - pointer-events: none; -} - -rect.area-building-yes { - opacity: 0.5; -} -text.area-building-yes { -} - -text.point-shop,text.point-amenity{ +text.point-shop, +text.point-amenity { font-size: 9px; } diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 2144946ca..a4e4eb93e 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -28,6 +28,10 @@ iD.svg.Labels = function(projection) { 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]; @@ -37,94 +41,70 @@ iD.svg.Labels = function(projection) { else return size / 3 * 2 * text.length; } - function drawLineLabels(group, defs, labels, filter, classes, position) { - - var uses = defs.selectAll('path.label') - .filter(filter) - .data(labels, iD.Entity.key); - - uses.enter() - .append('path') - .attr('class', 'label'); - - uses - .attr('id', function(d) { return 'labelshadow-' + d.id; }) - .attr('d', position('lineString')); - - uses.exit().remove(); - - var reverse = position('reverse'), - getClasses = position('classes'); + function drawLineLabels(group, entities, filter, classes, labels) { var texts = group.selectAll('text.' + classes) .filter(filter) - .data(labels, iD.Entity.key) + .data(entities, iD.Entity.key) var tp = texts.enter() .append('text') - .attr({ 'class': function(d, i) { return classes + ' ' + getClasses(d, i);}}) + .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'];}) .append('textPath') - .attr({ - 'class': 'textpath' - }); + .attr('class', 'textpath'); - var tps = group.selectAll('.textpath-label .textpath') - .data(labels) + var tps = texts.selectAll('.textpath') + .filter(filter) + .data(entities, iD.Entity.key) .attr({ - 'startOffset': position('startOffset'), - 'xlink:href': function(d, i) { return '#labelshadow-' + d.id}, - 'dominant-baseline': 'middle' + 'startOffset': '50%', + 'xlink:href': function(d, i) { return '#halo-' + d.id} }) - .text(function(d, i) { - return reverse(d, i) ? d.tags.name.split('').reverse().join('') : d.tags.name; - }); - - texts.each(function(d, i) { textWidth(d.tags.name, position('height')(d, i), this); }); + .text(function(d, i) { return d.tags.name }); texts.exit().remove(); } - function drawLineHalos(group, labels, filter, classes, position) { + function drawLineHalos(group, entities, filter, classes, labels) { var halos = group.selectAll('path') .filter(filter) - .data(labels, iD.Entity.key); + .data(entities, iD.Entity.key); halos.enter() .append('path') - .style({ - 'stroke-width': position('font-size') - }) + .style('stroke-width', get(labels, 'font-size')) + .attr('id', function(d, i) { return 'halo-' + d.id }) .attr('class', classes); - halos.attr('d', position('lineString')); + halos.attr('d', get(labels, 'lineString')); halos.exit().remove(); } - function drawPointHalos(group, labels, filter, classes, position) { + function drawPointHalos(group, entities, filter, classes, labels) { var halos = group.selectAll('rect.' + classes) .filter(filter) - .data(labels, iD.Entity.key); + .data(entities, iD.Entity.key); halos.enter() - .insert('rect', '.text-label') - .attr('class', function(d, i) { return classes + ' ' + position('classes')(d, i);}); + .append('rect') + .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'];}); halos.attr({ 'x': function(d, i) { - var x = position('x')(d, i) - 2; - if (position('textAnchor')(d, i) === 'middle') { - x -= textWidth(d.tags.name, position('height')(d, i)) / 2; + 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 position('y')(d, i) - position('height')(d, i) + 1 - 2; }, - 'width': function(d, i) { return textWidth(d.tags.name, position('height')(d, i)) + 4 }, - 'height': function(d, i) { return position('height')(d, i) + 4 }, + 'y': function(d, i) { return labels[i]['y'] - labels[i]['height'] + 1 - 2; }, + 'width': function(d, i) { return textWidth(d.tags.name, labels[i]['height']) + 4 }, + 'height': function(d, i) { return labels[i]['height'] + 4 }, 'fill': 'white', }); @@ -132,23 +112,22 @@ iD.svg.Labels = function(projection) { } - function drawPointLabels(group, labels, filter, classes, position) { - var getClasses = position('classes'); + function drawPointLabels(group, entities, filter, classes, labels) { var texts = group.selectAll('text.' + classes) .filter(filter) - .data(labels, iD.Entity.key); + .data(entities, iD.Entity.key); texts.enter() .append('text') - .attr('class', function(d, i) { return classes + ' ' + getClasses(d, i);}); + .attr('class', function(d, i) { return classes + ' ' + labels[i]['classes'] }) - texts.attr('x', position('x')) - .attr('y', position('y')) - .attr('transform', position('transform')) - .style('text-anchor', position('textAnchor')) + 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, position('height')(d, i), this); }); + .each(function(d, i) { textWidth(d.tags.name, labels[i]['height'], this); }); texts.exit().remove(); return texts; @@ -189,7 +168,7 @@ iD.svg.Labels = function(projection) { nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), nodes[i][1] + portion * (nodes[i + 1][1] - nodes[i][1]) ]; - i1 = i; + i1 = i + 1; } sofar += current; @@ -210,6 +189,7 @@ iD.svg.Labels = function(projection) { 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; @@ -236,6 +216,7 @@ iD.svg.Labels = function(projection) { 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 ++) { @@ -324,26 +305,14 @@ iD.svg.Labels = function(projection) { return v; } - function pointposition(attr) { - return function(d, i) { return positions['point'][i][attr] }; - } - function lineposition(attr) { - return function(d, i) { return positions['line'][i][attr] }; - } - function areaposition(attr) { - return function(d, i) { return positions['area'][i][attr] }; - } - - var label = surface.select('.layer-label'), halo = surface.select('.layer-halo'), - defs = surface.select('defs'), - points = drawPointLabels(label, labelled['point'], filter, 'text-label', pointposition), - pointHalos = drawPointHalos(label, labelled['point'], filter, 'point-label-halo', pointposition), - lines = drawLineLabels(label, defs, labelled['line'], filter, 'textpath-label', lineposition), - linesHalos = drawLineHalos(halo, labelled['line'], filter, 'textpath-label', lineposition), - areas = drawPointLabels(label, labelled['area'], filter, 'text-arealabel', areaposition), - areaHalos = drawPointHalos(label, labelled['area'], filter, 'area-label-halo', areaposition); + 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 5443db8c7..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', 'label']); + .data(['shadow', 'fill', 'casing', 'stroke', 'text', 'hit', 'halo', 'label']); layers.enter().append('g') .attr('class', function(d) { return 'layer layer-' + d; }); From 4714763ea316935821660c830ed7c4073c85ef71 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:18:57 -0500 Subject: [PATCH 18/25] Add a margin for point labels --- js/id/svg/labels.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index a4e4eb93e..ddf1289f1 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -240,6 +240,7 @@ iD.svg.Labels = function(projection) { function getPointLabel(entity, width, height) { var coord = projection(entity.loc), + m = 5, // margin offset = pointOffsets[0], p = { height: height, @@ -248,7 +249,7 @@ iD.svg.Labels = function(projection) { y: coord[1] + offset[1], textAnchor: offset[2] } - var rect = new RTree.Rectangle(p.x, p.y, width, height); + var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m); if (tryInsert(rect)) return p; } From 717af998f6ebf86afee1c1964236f210be60eb14 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:22:38 -0500 Subject: [PATCH 19/25] Undo line IDs --- js/id/svg/lines.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 3ae499b20..e3563bae9 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -34,14 +34,13 @@ iD.svg.Lines = function(projection) { } return function drawLines(surface, graph, entities, filter) { - function drawPaths(group, lines, filter, classes, lineString, prefix) { + function drawPaths(group, lines, filter, classes, lineString) { var paths = group.selectAll('path') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') - .attr('id', function(d) { return prefix + d.id;}) .attr('class', classes); paths @@ -81,9 +80,9 @@ iD.svg.Lines = function(projection) { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString, 'shadow-'), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString, 'casing-'), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString, 'stroke-'); + shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString), + casings = drawPaths(casing, lines, filter, 'way line casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, From b0de58b4552e1a71dcf416cd07cb8ba5820faa01 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:29:52 -0500 Subject: [PATCH 20/25] Fix merge (util.geo -> geo) --- js/id/geo.js | 35 ++++++++++++++ js/id/svg/labels.js | 4 +- js/id/util.js | 111 -------------------------------------------- 3 files changed, 37 insertions(+), 113 deletions(-) diff --git a/js/id/geo.js b/js/id/geo.js index b05a185fa..e44b182a6 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -73,3 +73,38 @@ iD.geo.polygonIntersectsPolygon = function(outer, inner) { return iD.geo.pointInPolygon(point, outer); }); }; + +// May have issues with self interesecting polygons +iD.geo.polygonCentroid = function(polygon) { + var x = 0, + y = 0, + area = iD.geo.area(polygon); + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + x += (xi + xj) * (xj * yi - xi * yj); + y += (yi + yj) * (xj * yi - xi * yj); + } + return [x / 6 / area, y / 6 / area]; +}; + +iD.geo.area = function(polygon) { + var area = 0; + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + area += xj * yi - xi * yj; + } + return area/2; +}; + +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/svg/labels.js b/js/id/svg/labels.js index ddf1289f1..d50ecfc97 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -255,7 +255,7 @@ iD.svg.Labels = function(projection) { function getLineLabel(entity, width, height) { var nodes = _.pluck(entity.nodes, 'loc').map(projection), - length = iD.util.geo.pathLength(nodes); + length = iD.geo.pathLength(nodes); if (length < width + 20) return; // 50, 40, 60, 30, 70 @@ -284,7 +284,7 @@ iD.svg.Labels = function(projection) { function getAreaLabel(entity, width, height) { var nodes = _.pluck(entity.nodes, 'loc') .map(iD.svg.RoundProjection(projection)), - centroid = iD.util.geo.polygonCentroid(nodes), + centroid = iD.geo.polygonCentroid(nodes), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; diff --git a/js/id/util.js b/js/id/util.js index b786dd0ba..a9ccd104f 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -82,114 +82,3 @@ iD.util.getStyle = function(selector) { } } }; - -iD.util.geo = {}; - -iD.util.geo.roundCoords = function(c) { - return [Math.floor(c[0]), Math.floor(c[1])]; -}; - -iD.util.geo.interp = function(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; -}; - -iD.util.geo.dist = function(a, b) { - return Math.sqrt(Math.pow(a[0] - b[0], 2) + - Math.pow(a[1] - b[1], 2)); -}; - -iD.util.geo.chooseIndex = function(way, point, map) { - var dist = iD.util.geo.dist, - projNodes = way.nodes.map(function(n) { - return map.projection(n.loc); - }); - - for (var i = 0, changes = []; i < projNodes.length - 1; i++) { - changes[i] = - (dist(projNodes[i], point) + dist(point, projNodes[i + 1])) / - dist(projNodes[i], projNodes[i + 1]); - } - - var idx = _.indexOf(changes, _.min(changes)), - ratio = dist(projNodes[idx], point) / dist(projNodes[idx], projNodes[idx + 1]), - loc = iD.util.geo.interp(way.nodes[idx].loc, way.nodes[idx + 1].loc, ratio); - - return { - index: idx + 1, - loc: loc - }; -}; - -// Return whether point is contained in polygon. -// -// `point` should be a 2-item array of coordinates. -// `polygon` should be an array of 2-item arrays of coordinates. -// -// From https://github.com/substack/point-in-polygon. -// ray-casting algorithm based on -// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -// -iD.util.geo.pointInPolygon = function(point, polygon) { - var x = point[0], - y = point[1], - inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) != (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; -}; - -iD.util.geo.polygonContainsPolygon = function(outer, inner) { - return _.every(inner, function (point) { - return iD.util.geo.pointInPolygon(point, outer); - }); -}; - -iD.util.geo.polygonIntersectsPolygon = function(outer, inner) { - return _.some(inner, function (point) { - return iD.util.geo.pointInPolygon(point, outer); - }); -}; - -// May have issues with self interesecting polygons -iD.util.geo.polygonCentroid = function(polygon) { - var x = 0, - y = 0, - area = iD.util.geo.area(polygon); - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - x += (xi + xj) * (xj * yi - xi * yj); - y += (yi + yj) * (xj * yi - xi * yj); - } - return [x / 6 / area, y / 6 / area]; -}; - -iD.util.geo.area = function(polygon) { - var area = 0; - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - area += xj * yi - xi * yj; - } - return area/2; -}; - -iD.util.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; -}; From 8b43094566bf8241e80ce43ad2435c4610e49ab7 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:34:23 -0500 Subject: [PATCH 21/25] Fix tests --- test/index.html | 2 ++ 1 file changed, 2 insertions(+) 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 @@ + From d387a311770d32f57a7b086d5bceea56bb1d25f9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 16:59:08 -0500 Subject: [PATCH 22/25] Add tests for new geo utils --- test/spec/util.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/spec/util.js b/test/spec/util.js index c2c645673..24f7e6ca7 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -112,5 +112,26 @@ describe('iD.Util', function() { expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; }); }); + + describe("#area", function() { + it('calculates the area of a trapezoid', function() { + var polygon = [[0, 0], [3, 0], [2, 2], [1, 2]]; + expect(iD.geo.area(polygon)).to.eql(4); + }); + }); + + describe('#polygonCentroid', function() { + it('calculates the centroid of a square', function() { + var square = [[0, 0], [0, 2], [2, 2], [2, 0]]; + expect(iD.geo.polygonCentroid(square)).to.eql([1, 1]); + }); + }); + + 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); + }); + }); }); }); From d14fdfa828f5c05e8cd1fb01e1ee60dbd4305d52 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 18:39:14 -0500 Subject: [PATCH 23/25] Remove geo.centroid(), use d3's instead --- js/id/geo.js | 24 ------------------------ js/id/svg/labels.js | 2 +- test/spec/util.js | 14 -------------- 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/js/id/geo.js b/js/id/geo.js index e44b182a6..08df659b0 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -74,30 +74,6 @@ iD.geo.polygonIntersectsPolygon = function(outer, inner) { }); }; -// May have issues with self interesecting polygons -iD.geo.polygonCentroid = function(polygon) { - var x = 0, - y = 0, - area = iD.geo.area(polygon); - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - x += (xi + xj) * (xj * yi - xi * yj); - y += (yi + yj) * (xj * yi - xi * yj); - } - return [x / 6 / area, y / 6 / area]; -}; - -iD.geo.area = function(polygon) { - var area = 0; - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - area += xj * yi - xi * yj; - } - return area/2; -}; - iD.geo.pathLength = function(path) { var length = 0, dx, dy; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index d50ecfc97..0db2c59e2 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -284,7 +284,7 @@ iD.svg.Labels = function(projection) { function getAreaLabel(entity, width, height) { var nodes = _.pluck(entity.nodes, 'loc') .map(iD.svg.RoundProjection(projection)), - centroid = iD.geo.polygonCentroid(nodes), + centroid = d3.geom.polygon(nodes).centroid(), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; diff --git a/test/spec/util.js b/test/spec/util.js index 24f7e6ca7..9d5cf082b 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -113,20 +113,6 @@ describe('iD.Util', function() { }); }); - describe("#area", function() { - it('calculates the area of a trapezoid', function() { - var polygon = [[0, 0], [3, 0], [2, 2], [1, 2]]; - expect(iD.geo.area(polygon)).to.eql(4); - }); - }); - - describe('#polygonCentroid', function() { - it('calculates the centroid of a square', function() { - var square = [[0, 0], [0, 2], [2, 2], [2, 0]]; - expect(iD.geo.polygonCentroid(square)).to.eql([1, 1]); - }); - }); - describe('#pathLength', function() { it('calculates a simple path length', function() { var path = [[0, 0], [0, 1], [3, 5]]; From 99748dba4e0d3c874273b7adce24f08410651a1e Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 18:54:31 -0500 Subject: [PATCH 24/25] Fix several small label related bugs --- css/map.css | 8 ++++---- js/id/svg/labels.js | 13 ++++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/css/map.css b/css/map.css index 594b23b61..06afe4584 100644 --- a/css/map.css +++ b/css/map.css @@ -586,18 +586,18 @@ text.pointlabel { .pointlabel-halo, .linelabel-halo, -.area-halo { +.arealabel-halo { opacity: 0.7; pointer-events: none; } -text.area-leisure-park { +text.area.tag-leisure-park { font-size: 16px; } -text.point-shop, -text.point-amenity { +text.point.tag-shop, +text.point.tag-amenity { font-size: 9px; } diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 0db2c59e2..44e8bad27 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -12,16 +12,17 @@ iD.svg.Labels = function(projection) { var default_size = 12; var font_sizes = label_stack.map(function(d) { - var style = iD.util.getStyle('text.' + d.join('-')); + 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 = [ - [10, 3, 'start'], // right - [10, 0, 'start'], - [-15, 0, 'end'], // left + [15, 3, 'start'], // right + [10, 0, 'start'], // unused right now + [-15, 0, 'end'] ]; var lineOffsets = [ @@ -103,6 +104,8 @@ iD.svg.Labels = function(projection) { 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', @@ -231,7 +234,7 @@ iD.svg.Labels = function(projection) { p = getAreaLabel(entity, width, font_size); } if (p) { - p.classes = label_stack[k].join('-'); + p.classes = entity.geometry() + ' tag-' + label_stack[k].slice(1).join('-'); positions[entity.geometry()].push(p); labelled[entity.geometry()].push(entity); } From 9008779a187d61639671e106e6d413b8efa3b5a3 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 23 Jan 2013 19:10:10 -0500 Subject: [PATCH 25/25] Fix point label hiding --- js/id/svg/labels.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 44e8bad27..6dcd3ec4c 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -187,7 +187,7 @@ iD.svg.Labels = function(projection) { return function drawLabels(surface, graph, entities, filter) { var rtree = new RTree(); - var hidePoints = !d3.select('.point').node(); + var hidePoints = !d3.select('.node.point').node(); var labelable = []; for (var i = 0; i < label_stack.length; i++) labelable.push([]);