map: place labels without overlaps

This commit is contained in:
Nils Schneider 2015-04-20 02:51:27 +02:00
parent 879c1e6f25
commit db3d8fa795
2 changed files with 186 additions and 48 deletions

View file

@ -228,7 +228,7 @@ define(["map/clientlayer", "map/labelslayer",
clientLayer.addTo(map) clientLayer.addTo(map)
clientLayer.setZIndex(5) clientLayer.setZIndex(5)
var labelsLayer = new LabelsLayer({minZoom: 15}) var labelsLayer = new LabelsLayer()
labelsLayer.addTo(map) labelsLayer.addTo(map)
labelsLayer.setZIndex(6) labelsLayer.setZIndex(6)
@ -368,22 +368,14 @@ define(["map/clientlayer", "map/labelslayer",
groupLost = L.featureGroup(markersLost).addTo(map) groupLost = L.featureGroup(markersLost).addTo(map)
var rtreeOnlineAll = rbush(9) 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)) 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) clientLayer.setData(rtreeOnlineAll)
labelsLayer.setData({online: rtreeOnline, labelsLayer.setData({online: nodesOnline.filter(has_location),
offline: rtreeOffline, offline: nodesOffline.filter(has_location),
new: rtreeNew, new: data.nodes.new.filter(has_location),
lost: rtreeLost lost: data.nodes.lost.filter(has_location)
}) })
updateView(true) updateView(true)

View file

@ -1,11 +1,169 @@
define(["leaflet"], define(["leaflet", "rbush"],
function (L) { function (L, rbush) {
return L.TileLayer.Canvas.extend({ 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) { setData: function (d) {
this.data = 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() this.redraw()
}, },
drawTile: function (canvas, tilePoint) { drawTile: function (canvas, tilePoint, zoom) {
function getTileBBox(s, map, tileSize, margin) { function getTileBBox(s, map, tileSize, margin) {
var tl = map.unproject([s.x - margin, s.y - margin]) var tl = map.unproject([s.x - margin, s.y - margin])
var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize]) 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] return [br.lat, tl.lng, tl.lat, br.lng]
} }
if (!this.data) if (!this.labels)
return return
var tileSize = this.options.tileSize var tileSize = this.options.tileSize
@ -21,50 +179,38 @@ define(["leaflet"],
var map = this._map var map = this._map
function projectNodes(d) { 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.x -= s.x
p.y -= s.y 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, this.margin)
var bbox = getTileBBox(s, map, tileSize, margin)
var nodesOnline = this.data.online.search(bbox).map(projectNodes) var labels = this.labels.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 ctx = canvas.getContext("2d") var ctx = canvas.getContext("2d")
ctx.font = "12px Roboto" ctx.font = font
ctx.textBaseline = "middle" ctx.lineWidth = 5
ctx.textAlign = "left" ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
ctx.lineWidth = 2.5 ctx.miterLimit = 2
var distance = 10
function drawLabel(d) { function drawLabel(d) {
ctx.strokeText(d.o.nodeinfo.hostname, d.p.x + distance, d.p.y) ctx.textAlign = d.label.anchor[0]
ctx.fillText(d.o.nodeinfo.hostname, d.p.x + distance, d.p.y) 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)" labels.filter(function (d) {
ctx.strokeStyle = "rgba(255, 255, 255, 0.6)" return zoom >= d.label.minZoom
nodesOffline.forEach(drawLabel) }).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)
} }
}) })
return c
}) })