render svg treemap
This commit is contained in:
parent
9ce19debd8
commit
b8c137efaf
|
@ -26,6 +26,11 @@
|
||||||
|
|
||||||
{{ block "update" .Update }}<div id="updatemsg" class="card {{ .Type }}" hx-swap-oob="morph">{{ with .Content }}{{ . }}{{ end }}</div>{{ end }}
|
{{ block "update" .Update }}<div id="updatemsg" class="card {{ .Type }}" hx-swap-oob="morph">{{ with .Content }}{{ . }}{{ end }}</div>{{ end }}
|
||||||
|
|
||||||
|
{{ block "svg" .SVG }}
|
||||||
|
<div id="treemap" hx-swap-oob="morph">
|
||||||
|
{{ . }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
{{ block "used" .Api }}
|
{{ block "used" .Api }}
|
||||||
<div id="state" hx-swap-oob="morph">
|
<div id="state" hx-swap-oob="morph">
|
||||||
|
|
||||||
|
|
|
@ -159,7 +159,7 @@ func (t *tree) Prefix() netip.Prefix {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tree) Split() {
|
func (t *tree) Split() {
|
||||||
lo, hi := splitPrefix(t.prefix)
|
lo, hi := SplitPrefix(t.prefix)
|
||||||
|
|
||||||
t.lo = &tree{prefix: lo, parent: t}
|
t.lo = &tree{prefix: lo, parent: t}
|
||||||
t.hi = &tree{prefix: hi, parent: t}
|
t.hi = &tree{prefix: hi, parent: t}
|
||||||
|
@ -214,7 +214,7 @@ func (t *tree) Alloc(bits int) *tree {
|
||||||
}
|
}
|
||||||
|
|
||||||
for best.prefix.Bits() < bits {
|
for best.prefix.Bits() < bits {
|
||||||
lo, hi := splitPrefix(best.prefix)
|
lo, hi := SplitPrefix(best.prefix)
|
||||||
//log.Println(lo, hi)
|
//log.Println(lo, hi)
|
||||||
switch {
|
switch {
|
||||||
case best.lo == nil:
|
case best.lo == nil:
|
||||||
|
@ -236,7 +236,7 @@ func (t *tree) Insert(p netip.Prefix) *tree {
|
||||||
var ret *tree
|
var ret *tree
|
||||||
|
|
||||||
for t.prefix.Bits() < p.Bits() {
|
for t.prefix.Bits() < p.Bits() {
|
||||||
lo, hi := splitPrefix(t.prefix)
|
lo, hi := SplitPrefix(t.prefix)
|
||||||
//log.Println(lo, hi)
|
//log.Println(lo, hi)
|
||||||
switch {
|
switch {
|
||||||
case lo.Overlaps(p):
|
case lo.Overlaps(p):
|
||||||
|
@ -435,6 +435,11 @@ func (db *DB) Find(p netip.Prefix) bool {
|
||||||
p = p.Masked()
|
p = p.Masked()
|
||||||
return db.provisions.Find(p) != nil
|
return db.provisions.Find(p) != nil
|
||||||
}
|
}
|
||||||
|
func (db *DB) Root() netip.Prefix {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
return db.provisions.prefix
|
||||||
|
}
|
||||||
|
|
||||||
//func (db *DB) Provision(p netip.Prefix) error {
|
//func (db *DB) Provision(p netip.Prefix) error {
|
||||||
// db.Lock()
|
// db.Lock()
|
||||||
|
@ -446,7 +451,7 @@ func (db *DB) Find(p netip.Prefix) bool {
|
||||||
// return nil
|
// return nil
|
||||||
//}
|
//}
|
||||||
|
|
||||||
func splitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) {
|
func SplitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) {
|
||||||
if p.IsSingleIP() {
|
if p.IsSingleIP() {
|
||||||
log.Fatal("can't split single ip prefix", p)
|
log.Fatal("can't split single ip prefix", p)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ func TestSplitPrefix(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
lo, hi := splitPrefix(netip.MustParsePrefix(test.in))
|
lo, hi := SplitPrefix(netip.MustParsePrefix(test.in))
|
||||||
if lo != netip.MustParsePrefix(test.lo) || hi != netip.MustParsePrefix(test.hi) {
|
if lo != netip.MustParsePrefix(test.lo) || hi != netip.MustParsePrefix(test.hi) {
|
||||||
t.Fatal(test.in, lo, hi)
|
t.Fatal(test.in, lo, hi)
|
||||||
}
|
}
|
||||||
|
|
94
main.go
94
main.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.freifunk-franken.de/jkimmel/sub/ipalloc"
|
"git.freifunk-franken.de/jkimmel/sub/ipalloc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,6 +51,7 @@ func (s *server) apiPrefix(w http.ResponseWriter, r *http.Request) {
|
||||||
type PageContent struct {
|
type PageContent struct {
|
||||||
Api *ipalloc.DB
|
Api *ipalloc.DB
|
||||||
Update UpdateMessage
|
Update UpdateMessage
|
||||||
|
SVG template.HTML
|
||||||
}
|
}
|
||||||
type UpdateMessage struct {
|
type UpdateMessage struct {
|
||||||
Type string
|
Type string
|
||||||
|
@ -57,7 +59,7 @@ type UpdateMessage struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) template(w http.ResponseWriter, r *http.Request) {
|
func (s *server) template(w http.ResponseWriter, r *http.Request) {
|
||||||
err := s.tmpl.Execute(w, PageContent{Api: s.ipdb})
|
err := s.tmpl.Execute(w, PageContent{Api: s.ipdb, SVG: renderSVG(s.ipdb.Used(), s.ipdb.Root())})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
@ -101,6 +103,7 @@ func (s *server) apiAlloc(w http.ResponseWriter, r *http.Request) {
|
||||||
updmsg.Type = "hint"
|
updmsg.Type = "hint"
|
||||||
updmsg.Content = fmt.Sprintf("Added %s", alloc.Prefix())
|
updmsg.Content = fmt.Sprintf("Added %s", alloc.Prefix())
|
||||||
}
|
}
|
||||||
|
s.tmpl.ExecuteTemplate(w, "svg", renderSVG(s.ipdb.Used(), s.ipdb.Root()))
|
||||||
if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil {
|
if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
@ -132,6 +135,7 @@ func (s *server) apiDealloc(w http.ResponseWriter, r *http.Request) {
|
||||||
updmsg.Type = "hint"
|
updmsg.Type = "hint"
|
||||||
updmsg.Content = fmt.Sprintf("Removed %s", prefix)
|
updmsg.Content = fmt.Sprintf("Removed %s", prefix)
|
||||||
}
|
}
|
||||||
|
s.tmpl.ExecuteTemplate(w, "svg", renderSVG(s.ipdb.Used(), s.ipdb.Root()))
|
||||||
s.tmpl.ExecuteTemplate(w, "update", updmsg)
|
s.tmpl.ExecuteTemplate(w, "update", updmsg)
|
||||||
s.tmpl.ExecuteTemplate(w, "used", s.ipdb)
|
s.tmpl.ExecuteTemplate(w, "used", s.ipdb)
|
||||||
}
|
}
|
||||||
|
@ -155,3 +159,91 @@ func main() {
|
||||||
s.setup()
|
s.setup()
|
||||||
http.ListenAndServe("[::1]:8080", &s)
|
http.ListenAndServe("[::1]:8080", &s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rect struct {
|
||||||
|
y, x, h, w int
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefixShape(p netip.Prefix) rect {
|
||||||
|
bits := p.Addr().BitLen() - p.Bits()
|
||||||
|
|
||||||
|
return rect{
|
||||||
|
y: 0,
|
||||||
|
x: 0,
|
||||||
|
h: 1 << (bits / 2),
|
||||||
|
w: 1 << ((bits + 1) / 2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeInto(p, into netip.Prefix) rect {
|
||||||
|
return placeIntoRect(p, into, prefixShape(into))
|
||||||
|
}
|
||||||
|
func placeIntoRect(p, into netip.Prefix, r rect) rect {
|
||||||
|
if p == into {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
if p.Bits() < into.Bits() {
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
lo, hi := ipalloc.SplitPrefix(into)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case lo.Overlaps(p):
|
||||||
|
var subr rect
|
||||||
|
if r.w > r.h {
|
||||||
|
subr.y = r.y
|
||||||
|
subr.x = r.x
|
||||||
|
subr.h = r.h
|
||||||
|
subr.w = r.w / 2
|
||||||
|
} else {
|
||||||
|
subr.y = r.y
|
||||||
|
subr.x = r.x
|
||||||
|
subr.h = r.h / 2
|
||||||
|
subr.w = r.w
|
||||||
|
}
|
||||||
|
return placeIntoRect(p, lo, subr)
|
||||||
|
case hi.Overlaps(p):
|
||||||
|
var subr rect
|
||||||
|
if r.w > r.h {
|
||||||
|
subr.y = r.y
|
||||||
|
subr.x = (r.x + (r.x + r.w)) / 2
|
||||||
|
subr.h = r.h
|
||||||
|
subr.w = r.w / 2
|
||||||
|
} else {
|
||||||
|
subr.y = (r.y + (r.y + r.h)) / 2
|
||||||
|
subr.x = r.x
|
||||||
|
subr.h = r.h / 2
|
||||||
|
subr.w = r.w
|
||||||
|
}
|
||||||
|
return placeIntoRect(p, hi, subr)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func renderSVG(ps []netip.Prefix, into netip.Prefix) template.HTML {
|
||||||
|
var b strings.Builder
|
||||||
|
shape := prefixShape(into)
|
||||||
|
fmt.Fprintf(&b, "<svg viewBox=\"-1 -1 %d %d\">\n", shape.w+2, shape.h+2)
|
||||||
|
fmt.Fprintf(&b, " <defs>")
|
||||||
|
fmt.Fprintf(&b, " <filter id=\"shadow\">")
|
||||||
|
fmt.Fprintf(&b, " <feGaussianBlur stdDeviation=\"0.1\" />")
|
||||||
|
fmt.Fprintf(&b, " </filter>")
|
||||||
|
fmt.Fprintf(&b, `</defs>`)
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
place := placeInto(p, into)
|
||||||
|
fmt.Fprintf(&b, " <g style=\"fill:gray; filter:url(#shadow);\" transform=\"translate(0.1 0.1)\">\n")
|
||||||
|
fmt.Fprintf(&b, " <rect x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"0.2\" />\n", place.x, place.y, place.w, place.h)
|
||||||
|
fmt.Fprintf(&b, " </g>\n")
|
||||||
|
}
|
||||||
|
for _, p := range ps {
|
||||||
|
place := placeInto(p, into)
|
||||||
|
fmt.Fprintf(&b, " <g style=\"fill:var(--cfg); stroke:white; stroke-width:0.1;\">\n")
|
||||||
|
fmt.Fprintf(&b, " <title>%s</title>\n", p)
|
||||||
|
fmt.Fprintf(&b, " <rect hx-get=\"/api/dealloc/%s\" hx-swap=\"none\" x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" rx=\"0.2\"/>\n", p, place.x, place.y, place.w, place.h)
|
||||||
|
fmt.Fprintf(&b, " </g>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, `</svg>`)
|
||||||
|
return template.HTML(b.String())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user