From db3d8fa795197494b7f04bf47eafa870743ec37e Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Mon, 20 Apr 2015 02:51:27 +0200 Subject: [PATCH] map: place labels without overlaps --- lib/map.js | 18 +--- lib/map/labelslayer.js | 216 ++++++++++++++++++++++++++++++++++------- 2 files changed, 186 insertions(+), 48 deletions(-) diff --git a/lib/map.js b/lib/map.js index ef7a74c..3263275 100644 --- a/lib/map.js +++ b/lib/map.js @@ -228,7 +228,7 @@ define(["map/clientlayer", "map/labelslayer", clientLayer.addTo(map) clientLayer.setZIndex(5) - var labelsLayer = new LabelsLayer({minZoom: 15}) + var labelsLayer = new LabelsLayer() labelsLayer.addTo(map) labelsLayer.setZIndex(6) @@ -368,22 +368,14 @@ define(["map/clientlayer", "map/labelslayer", groupLost = L.featureGroup(markersLost).addTo(map) var rtreeOnlineAll = rbush(9) - var rtreeOnline = rbush(9) - var rtreeOffline = rbush(9) - var rtreeNew = rbush(9) - var rtreeLost = rbush(9) rtreeOnlineAll.load(data.nodes.all.filter(online).filter(has_location).map(mapRTree)) - rtreeOnline.load(nodesOnline.filter(has_location).map(mapRTree)) - rtreeOffline.load(nodesOffline.filter(has_location).map(mapRTree)) - rtreeNew.load(data.nodes.new.filter(has_location).map(mapRTree)) - rtreeLost.load(data.nodes.lost.filter(has_location).map(mapRTree)) clientLayer.setData(rtreeOnlineAll) - labelsLayer.setData({online: rtreeOnline, - offline: rtreeOffline, - new: rtreeNew, - lost: rtreeLost + labelsLayer.setData({online: nodesOnline.filter(has_location), + offline: nodesOffline.filter(has_location), + new: data.nodes.new.filter(has_location), + lost: data.nodes.lost.filter(has_location) }) updateView(true) diff --git a/lib/map/labelslayer.js b/lib/map/labelslayer.js index 9585d50..f6c9a42 100644 --- a/lib/map/labelslayer.js +++ b/lib/map/labelslayer.js @@ -1,11 +1,169 @@ -define(["leaflet"], - function (L) { - return L.TileLayer.Canvas.extend({ +define(["leaflet", "rbush"], + function (L, rbush) { + var labelLocations = [["left", "middle", 0 / 8], + ["center", "top", 6 / 8], + ["right", "middle", 4 / 8], + ["left", "top", 7 / 8], + ["left", "ideographic", 1 / 8], + ["right", "top", 5 / 8], + ["center", "ideographic", 2 / 8], + ["right", "ideographic", 3 / 8]] + + var labelOffset = 8 + var font = "10px Roboto" + var labelHeight = 12 + var nodeRadius = 4 + + var ctx = document.createElement("canvas").getContext("2d") + + function measureText(font, text) { + ctx.font = font + return ctx.measureText(text) + } + + function mapRTree(d) { + var o = [d.position.lat, d.position.lng, d.position.lat, d.position.lng] + + o.label = d + + return o + } + + function prepareLabel(fillStyle) { + return function (d) { + return { position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude), + label: d.nodeinfo.hostname, + fillStyle: fillStyle, + height: labelHeight, + width: measureText(font, d.nodeinfo.hostname).width + } + } + } + + function calcOffset(loc) { + return [ labelOffset * Math.cos(loc[2] * 2 * Math.PI), + -labelOffset * Math.sin(loc[2] * 2 * Math.PI)] + } + + function labelRect(p, offset, anchor, label) { + var dx = { left: 0, + right: -label.width, + center: -label.width / 2 + } + + var dy = { top: 0, + ideographic: -label.height, + middle: -label.height / 2 + } + + var x = p.x + offset[0] + dx[anchor[0]] + var y = p.y + offset[1] + dy[anchor[1]] + + return [x, y, x + label.width, y + label.height] + } + + var c = L.TileLayer.Canvas.extend({ + onAdd: function (map) { + L.TileLayer.Canvas.prototype.onAdd.call(this, map) + if (this.data) + this.prepareLabels() + }, setData: function (d) { this.data = d + if (this._map) + this.prepareLabels() + }, + prepareLabels: function () { + var d = this.data + + // label: + // - position (WGS84 coords) + // - offset (2D vector in pixels) + // - anchor (tuple, textAlignment, textBaseline) + // - minZoom (inclusive) + // - label (string) + // - color (string) + + var labelsOnline = d.online.map(prepareLabel("rgba(0, 0, 0, 0.9)")) + var labelsOffline = d.offline.map(prepareLabel("rgba(212, 62, 42, 0.9)")) + var labelsNew = d.new.map(prepareLabel("rgba(85, 128, 32, 0.9)")) + var labelsLost = d.lost.map(prepareLabel("rgba(212, 62, 42, 0.9)")) + + var labels = [] + .concat(labelsNew) + .concat(labelsLost) + .concat(labelsOnline) + .concat(labelsOffline) + + var minZoom = this.options.minZoom + var maxZoom = this.options.maxZoom + + var trees = [] + + var map = this._map + + function nodeToRect(z) { + return function (d) { + var p = map.project(d.position, z) + return [p.x - nodeRadius, p.y - nodeRadius, + p.x + nodeRadius, p.y + nodeRadius] + } + } + + for (var z = minZoom; z <= maxZoom; z++) { + trees[z] = rbush(9) + trees[z].load(labels.map(nodeToRect(z))) + } + + labels.forEach(function (d) { + var best = labelLocations.map(function (loc) { + var offset = calcOffset(loc, d) + var z + + for (z = maxZoom; z >= minZoom; z--) { + var p = map.project(d.position, z) + var rect = labelRect(p, offset, loc, d) + var candidates = trees[z].search(rect) + + if (candidates.length > 0) + break + } + + return {loc: loc, z: z + 1} + }).filter(function (d) { + return d.z <= maxZoom + }).sort(function (a, b) { + return a.z - b.z + })[0] + + if (best === undefined) + return + + d.offset = calcOffset(best.loc, d) + d.minZoom = best.z + d.anchor = best.loc + + for (var z = maxZoom; z >= best.z; z--) { + var p = map.project(d.position, z) + var rect = labelRect(p, d.offset, best.loc, d) + trees[z].insert(rect) + } + }) + + labels = labels.filter(function (d) { + return d.minZoom !== undefined + }) + + this.margin = 16 + labels.map(function (d) { + return d.width + }).sort().reverse()[0] + + this.labels = rbush(9) + this.labels.load(labels.map(mapRTree)) + this.redraw() }, - drawTile: function (canvas, tilePoint) { + drawTile: function (canvas, tilePoint, zoom) { function getTileBBox(s, map, tileSize, margin) { var tl = map.unproject([s.x - margin, s.y - margin]) var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize]) @@ -13,7 +171,7 @@ define(["leaflet"], return [br.lat, tl.lng, tl.lat, br.lng] } - if (!this.data) + if (!this.labels) return var tileSize = this.options.tileSize @@ -21,50 +179,38 @@ define(["leaflet"], var map = this._map function projectNodes(d) { - var p = map.project([d.node.nodeinfo.location.latitude, d.node.nodeinfo.location.longitude]) + var p = map.project(d.label.position) p.x -= s.x p.y -= s.y - return {p: p, o: d.node} + return {p: p, label: d.label} } - var margin = 256 - var bbox = getTileBBox(s, map, tileSize, margin) + var bbox = getTileBBox(s, map, tileSize, this.margin) - var nodesOnline = this.data.online.search(bbox).map(projectNodes) - var nodesOffline = this.data.offline.search(bbox).map(projectNodes) - var nodesNew = this.data.new.search(bbox).map(projectNodes) - var nodesLost = this.data.lost.search(bbox).map(projectNodes) + var labels = this.labels.search(bbox).map(projectNodes) var ctx = canvas.getContext("2d") - ctx.font = "12px Roboto" - ctx.textBaseline = "middle" - ctx.textAlign = "left" - ctx.lineWidth = 2.5 + ctx.font = font + ctx.lineWidth = 5 + ctx.strokeStyle = "rgba(255, 255, 255, 0.8)" + ctx.miterLimit = 2 - var distance = 10 function drawLabel(d) { - ctx.strokeText(d.o.nodeinfo.hostname, d.p.x + distance, d.p.y) - ctx.fillText(d.o.nodeinfo.hostname, d.p.x + distance, d.p.y) + ctx.textAlign = d.label.anchor[0] + ctx.textBaseline = d.label.anchor[1] + ctx.fillStyle = d.label.fillStyle + ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]) + ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]) } - ctx.fillStyle = "rgba(212, 62, 42, 0.6)" - ctx.strokeStyle = "rgba(255, 255, 255, 0.6)" - nodesOffline.forEach(drawLabel) - - ctx.fillStyle = "rgba(0, 0, 0, 0.6)" - ctx.strokeStyle = "rgba(255, 255, 255, 0.9)" - nodesOnline.forEach(drawLabel) - - ctx.fillStyle = "rgba(212, 62, 42, 0.6)" - ctx.strokeStyle = "rgba(255, 255, 255, 0.9)" - nodesLost.forEach(drawLabel) - - ctx.fillStyle = "rgba(0, 0, 0, 0.6)" - ctx.strokeStyle = "rgba(255, 255, 255, 1.0)" - nodesNew.forEach(drawLabel) + labels.filter(function (d) { + return zoom >= d.label.minZoom + }).forEach(drawLabel) } }) + + return c })