map: place labels without overlaps
This commit is contained in:
parent
879c1e6f25
commit
db3d8fa795
18
lib/map.js
18
lib/map.js
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue