render svg treemap

This commit is contained in:
Johannes Kimmel 2023-11-09 16:32:30 +01:00
parent 9ce19debd8
commit b8c137efaf
4 changed files with 108 additions and 6 deletions

View File

@ -26,6 +26,11 @@
{{ 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 }}
<div id="state" hx-swap-oob="morph">

View File

@ -159,7 +159,7 @@ func (t *tree) Prefix() netip.Prefix {
}
func (t *tree) Split() {
lo, hi := splitPrefix(t.prefix)
lo, hi := SplitPrefix(t.prefix)
t.lo = &tree{prefix: lo, 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 {
lo, hi := splitPrefix(best.prefix)
lo, hi := SplitPrefix(best.prefix)
//log.Println(lo, hi)
switch {
case best.lo == nil:
@ -236,7 +236,7 @@ func (t *tree) Insert(p netip.Prefix) *tree {
var ret *tree
for t.prefix.Bits() < p.Bits() {
lo, hi := splitPrefix(t.prefix)
lo, hi := SplitPrefix(t.prefix)
//log.Println(lo, hi)
switch {
case lo.Overlaps(p):
@ -435,6 +435,11 @@ func (db *DB) Find(p netip.Prefix) bool {
p = p.Masked()
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 {
// db.Lock()
@ -446,7 +451,7 @@ func (db *DB) Find(p netip.Prefix) bool {
// return nil
//}
func splitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) {
func SplitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) {
if p.IsSingleIP() {
log.Fatal("can't split single ip prefix", p)
}

View File

@ -15,7 +15,7 @@ func TestSplitPrefix(t *testing.T) {
}
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) {
t.Fatal(test.in, lo, hi)
}

94
main.go
View File

@ -9,6 +9,7 @@ import (
"net/netip"
"strconv"
"strings"
"git.freifunk-franken.de/jkimmel/sub/ipalloc"
)
@ -50,6 +51,7 @@ func (s *server) apiPrefix(w http.ResponseWriter, r *http.Request) {
type PageContent struct {
Api *ipalloc.DB
Update UpdateMessage
SVG template.HTML
}
type UpdateMessage struct {
Type string
@ -57,7 +59,7 @@ type UpdateMessage struct {
}
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 {
log.Println(err)
}
@ -101,6 +103,7 @@ func (s *server) apiAlloc(w http.ResponseWriter, r *http.Request) {
updmsg.Type = "hint"
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 {
log.Println(err)
}
@ -132,6 +135,7 @@ func (s *server) apiDealloc(w http.ResponseWriter, r *http.Request) {
updmsg.Type = "hint"
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, "used", s.ipdb)
}
@ -155,3 +159,91 @@ func main() {
s.setup()
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())
}