ipplot.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. /*
  2. * This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  5. */
  6. package ipplot
  7. import (
  8. "errors"
  9. "image"
  10. "image/color"
  11. "log"
  12. "math"
  13. "net"
  14. "sort"
  15. "gioui.org/f32"
  16. "gioui.org/io/key"
  17. "gioui.org/io/pointer"
  18. "gioui.org/layout"
  19. "gioui.org/op"
  20. "gioui.org/op/clip"
  21. "gioui.org/op/paint"
  22. "gioui.org/unit"
  23. "gioui.org/widget/material"
  24. )
  25. var cursorBlock = &Block{blockType: Transient, ipW: 1, ipH: 1}
  26. /*
  27. There is no point in drawing blocks that we cannot see. For
  28. prefixes longer than 24 bits, as well as prefixes longer
  29. than 24 bits plus the current zoom level, we will skip the
  30. layout phase, possibly favoring aggregate effects, like
  31. bloom, as an indication of block population and density.
  32. */
  33. var drawHorizon int = 24
  34. /*
  35. Any more than 12 bits past the current zoom level, and
  36. blocks become increasingly difficult to identify, let alone
  37. click or tap. Better we cut off interaction at a sensible
  38. level, providing visual indicators of when blocks can or
  39. cannot be "touched."
  40. */
  41. var touchHorizon int = 12
  42. var ipv4IANAReservedStrings = []string{
  43. "0.0.0.0/8",
  44. "10.0.0.0/8",
  45. "172.16.0.0/12",
  46. "192.168.0.0/16",
  47. "100.64.0.0/10",
  48. "127.0.0.0/8",
  49. "169.254.0.0/16",
  50. "192.0.0.0/24",
  51. "192.0.2.0/24",
  52. "192.31.196.0/24",
  53. "192.52.193.0/24",
  54. "192.88.99.0/24",
  55. "192.175.48.0/24",
  56. "198.18.0.0/15",
  57. "198.51.0.0/24",
  58. "203.0.113.0/24",
  59. "224.0.0.0/4",
  60. "240.0.0.0/4",
  61. }
  62. type IPPlotState int
  63. const (
  64. StateNeutral IPPlotState = iota // Nothing happening
  65. StateZoom // Zoom view in/out
  66. StatePrimeBlock // Ready to act on pressed block
  67. StateSelectBlock // Update info for selected block
  68. StateMoveBlock // Permit block movement
  69. StateSelectOverlapping // Select block above or below current
  70. StateResizeBlock // Resize block in place
  71. )
  72. type BlockType int
  73. const (
  74. Allocation BlockType = iota // User allocation
  75. IANAPrivate // RFC1918
  76. IANAReserved // All other IANA reserved
  77. Transient // For intermediate calculations
  78. )
  79. type BlockOverlap int
  80. const (
  81. OverlapAny BlockOverlap = iota
  82. OverlapShorter // Cannot contain other blocks
  83. OverlapLonger // Cannot be contained by other blocks
  84. OverlapNone // Cannot overlap other blocks, at all
  85. )
  86. type BlockStatus int
  87. const (
  88. Nascent BlockStatus = iota // Not persistent
  89. Reserved // Persistent but not assigned
  90. Assigned // Persistent and assigned
  91. Decommissioned // Pending reassignment
  92. )
  93. type IPBlock interface {
  94. ContainsPoint(uint64, uint64) bool
  95. IPCoordinates() (uint64, uint64)
  96. IPDimensions() (uint64, uint64)
  97. LessThanOrEq(IPBlock) bool
  98. PixelCoordinates(pxMax image.Point) (unit.Dp, unit.Dp)
  99. PixelDimensions(pxMax image.Point) (unit.Dp, unit.Dp)
  100. ToPrefix() IPPrefix
  101. }
  102. type Block struct {
  103. blockType BlockType
  104. overlap BlockOverlap
  105. immutable bool
  106. status BlockStatus
  107. ipX uint64
  108. ipY uint64
  109. ipW uint64
  110. ipH uint64
  111. }
  112. func (b1 *Block) Eq(b2 *Block) bool {
  113. return b1.ipX == b2.ipX &&
  114. b1.ipY == b2.ipY &&
  115. b1.ipW == b2.ipW &&
  116. b1.ipH == b2.ipH
  117. }
  118. func (b1 *Block) LessThanOrEq(b2 IPBlock) bool {
  119. return b1.ToPrefix().LessThanOrEq(b2.ToPrefix())
  120. }
  121. func (b *Block) ContainsPoint(ipX, ipY uint64) bool {
  122. // <= (x+w-1) must be used because x+w can wrap to 0!
  123. return (b.ipX <= ipX) &&
  124. (ipX <= (b.ipX + b.ipW - 1)) &&
  125. (b.ipY <= ipY) &&
  126. (ipY <= (b.ipY + b.ipH - 1))
  127. }
  128. func (b *Block) IPCoordinates() (uint64, uint64) {
  129. return b.ipX, b.ipY
  130. }
  131. func (b *Block) IPDimensions() (uint64, uint64) {
  132. return b.ipW, b.ipH
  133. }
  134. func (b *Block) PixelCoordinates(
  135. pxMax image.Point,
  136. ) (unit.Dp, unit.Dp) {
  137. // TODO: assume IPv4, for now; support IPv6 later.
  138. ipMax := uint64(0xffff)
  139. minMax := minPixelMax(pxMax)
  140. return IPToPxCoords(b.ipX, b.ipY, ipMax, minMax)
  141. }
  142. func (b *Block) PixelDimensions(
  143. pxMax image.Point,
  144. ) (unit.Dp, unit.Dp) {
  145. // TODO: assume IPv4, for now; support IPv6 later.
  146. ipMax := uint64(0xffff)
  147. minMax := minPixelMax(pxMax)
  148. return IPToPxCoords(b.ipW, b.ipH, ipMax, minMax)
  149. }
  150. func (b *Block) ToPrefix() IPPrefix {
  151. var p IPPrefix = IPv4FromGeometry(
  152. b.ipX,
  153. b.ipY,
  154. b.ipW,
  155. b.ipH,
  156. )
  157. return p
  158. }
  159. func NewIPBlock(p IPPrefix, t BlockType) IPBlock {
  160. b := &Block{blockType: t}
  161. switch t {
  162. case Allocation:
  163. case IANAPrivate:
  164. b.overlap = OverlapLonger
  165. b.immutable = true
  166. b.status = Reserved
  167. case IANAReserved:
  168. b.overlap = OverlapNone
  169. b.immutable = true
  170. b.status = Reserved
  171. }
  172. b.ipX, b.ipY, b.ipW, b.ipH = p.ToGeometry()
  173. return b
  174. }
  175. func IPToPxCoords(
  176. x,
  177. y,
  178. ipMax uint64,
  179. pxMax int,
  180. ) (unit.Dp, unit.Dp) {
  181. dx := float64(pxMax) / float64(ipMax)
  182. dy := dx
  183. return unit.Dp(float64(x) * dx), unit.Dp(float64(y) * dy)
  184. }
  185. func PxToIPCoords(
  186. x,
  187. y float32,
  188. ipMax uint64,
  189. pxMax int,
  190. ) (uint64, uint64) {
  191. dx := float64(ipMax) / float64(pxMax)
  192. dy := dx
  193. return uint64(float64(x) * dx), uint64(float64(y) * dy)
  194. }
  195. func minPixelMax(pxMax image.Point) int {
  196. return int(math.Min(float64(pxMax.X), float64(pxMax.Y)))
  197. }
  198. type IPPlot struct {
  199. state IPPlotState
  200. selected int // index of selected block
  201. hovered int // index of hovered block
  202. zoomLevel int // display only prefixes of length >= 2z
  203. ptrX float32
  204. ptrY float32
  205. freshHovered bool // hover triggered; change to int flag?
  206. blocktip material.LabelStyle
  207. blocks []IPBlock
  208. }
  209. func (ipp *IPPlot) handleInputEvents(
  210. gtx layout.Context,
  211. maxPt int,
  212. ) {
  213. // If we hover over a block and nothing was hovered
  214. // before, then we say the hover event is "fresh." If we
  215. // are, instead, simply recalculating which block is being
  216. // hovered over, then we set "freshHovered" back to false.
  217. // This is a fairly opaque nomenclature, so
  218. // TODO: find a better name for "freshHovered"
  219. for _, e := range gtx.Events(ipp) {
  220. if e, ok := e.(pointer.Event); ok {
  221. switch {
  222. case ipp.state == StateNeutral && e.Type == pointer.Press:
  223. ipp.hovered =
  224. calculateHovered(
  225. ipp.blocks,
  226. ipp.ptrX,
  227. ipp.ptrY,
  228. maxPt,
  229. )
  230. ipp.freshHovered = true
  231. if ipp.hovered >= 0 {
  232. ipp.state = StatePrimeBlock
  233. }
  234. case e.Type == pointer.Move:
  235. ipp.freshHovered = false
  236. ipp.ptrX = e.Position.X
  237. ipp.ptrY = e.Position.Y
  238. }
  239. }
  240. }
  241. if !ipp.freshHovered {
  242. ipp.hovered =
  243. calculateHovered(
  244. ipp.blocks,
  245. ipp.ptrX,
  246. ipp.ptrY,
  247. maxPt,
  248. )
  249. }
  250. }
  251. func (ipp *IPPlot) subscribeToInputEvents(
  252. gtx layout.Context,
  253. maxPt int,
  254. ) {
  255. area := clip.Rect(image.Rect(
  256. 0,
  257. 0,
  258. maxPt,
  259. maxPt,
  260. )).Push(gtx.Ops)
  261. mask := pointer.Move
  262. mask |= pointer.Press
  263. mask |= pointer.Release
  264. pointer.InputOp{Tag: ipp, Types: mask}.Add(gtx.Ops)
  265. area.Pop()
  266. key.InputOp{Tag: ipp, Keys: "z|x"}.Add(gtx.Ops)
  267. }
  268. func (ipp *IPPlot) drawBlocks(gtx layout.Context) {
  269. for i, ipb := range ipp.blocks {
  270. x, y := ipb.PixelCoordinates(gtx.Constraints.Max)
  271. w, h := ipb.PixelDimensions(gtx.Constraints.Max)
  272. // <= (x+w-1) must be used because x+w can wrap to 0!
  273. minPt := f32.Pt(float32(x), float32(y))
  274. maxPt := f32.Pt(float32(x+w)-1, float32(y+h)-1)
  275. if i == ipp.hovered {
  276. drawHoveredBlock(gtx, minPt, maxPt)
  277. } else {
  278. drawIANAReservedBlock(gtx, minPt, maxPt)
  279. }
  280. }
  281. }
  282. func (ipp *IPPlot) drawBlocktip(
  283. gtx layout.Context,
  284. maxPt int,
  285. ) {
  286. var block IPBlock
  287. if ipp.hovered >= 0 {
  288. block = ipp.blocks[ipp.hovered]
  289. } else {
  290. block = cursorBlock
  291. // TODO: assume IPv4, for now; support IPv6 later.
  292. ipMax := uint64(0xffff)
  293. cursorBlock.ipX, cursorBlock.ipY =
  294. PxToIPCoords(ipp.ptrX, ipp.ptrY, ipMax, maxPt)
  295. }
  296. ipp.blocktip.Text = block.ToPrefix().String()
  297. defer op.Offset(image.Point{
  298. X: gtx.Dp(unit.Dp(ipp.ptrX) + 15),
  299. Y: gtx.Dp(unit.Dp(ipp.ptrY)),
  300. }).Push(gtx.Ops).Pop()
  301. ipp.blocktip.Layout(gtx)
  302. }
  303. func (ipp *IPPlot) Layout(gtx layout.Context) {
  304. maxPt := minPixelMax(gtx.Constraints.Max)
  305. ipp.handleInputEvents(gtx, maxPt)
  306. ipp.subscribeToInputEvents(gtx, maxPt)
  307. ipp.drawBlocks(gtx)
  308. ipp.drawBlocktip(gtx, maxPt)
  309. }
  310. func drawHoveredBlock(
  311. gtx layout.Context,
  312. minPt,
  313. maxPt f32.Point,
  314. ) {
  315. // TODO: extremely silly; fix this nonsense
  316. rect := clip.Rect(image.Rect(
  317. gtx.Dp(unit.Dp(minPt.X)),
  318. gtx.Dp(unit.Dp(minPt.Y)),
  319. gtx.Dp(unit.Dp(maxPt.X)),
  320. gtx.Dp(unit.Dp(maxPt.Y)),
  321. ))
  322. color := color.NRGBA{R: 0xa0, G: 0x70, B: 0x10, A: 0x50}
  323. border :=
  324. clip.Stroke{Path: rect.Path(), Width: 0.2}
  325. paint.FillShape(gtx.Ops, color, rect.Op())
  326. paint.FillShape(gtx.Ops, color, border.Op())
  327. }
  328. type f32Rect struct {
  329. Min, Max f32.Point
  330. }
  331. func (r f32Rect) Path(ops *op.Ops) clip.PathSpec {
  332. var p clip.Path
  333. p.Begin(ops)
  334. p.MoveTo(r.Min)
  335. p.Line(f32.Pt(r.Max.X, r.Min.Y))
  336. p.Line(r.Max)
  337. p.Line(f32.Pt(r.Min.X, r.Max.Y))
  338. p.Line(r.Min)
  339. return p.End()
  340. }
  341. func (r f32Rect) Op(ops *op.Ops) clip.Op {
  342. return clip.Outline{Path: r.Path(ops)}.Op()
  343. }
  344. func (r f32Rect) Push(ops *op.Ops) clip.Stack {
  345. return r.Op(ops).Push(ops)
  346. }
  347. func drawIANAReservedBlock(
  348. gtx layout.Context,
  349. minPt,
  350. maxPt f32.Point,
  351. ) {
  352. // TODO: extremely silly; fix this nonsense
  353. rect := clip.Rect(image.Rect(
  354. gtx.Dp(unit.Dp(minPt.X)),
  355. gtx.Dp(unit.Dp(minPt.Y)),
  356. gtx.Dp(unit.Dp(maxPt.X)),
  357. gtx.Dp(unit.Dp(maxPt.Y)),
  358. ))
  359. black := color.NRGBA{A: 0xff}
  360. blue := color.NRGBA{R: 0x10, G: 0x30, B: 0x50, A: 0x50}
  361. border :=
  362. clip.Stroke{Path: rect.Path(), Width: 0.2}
  363. paint.FillShape(gtx.Ops, blue, rect.Op())
  364. paint.FillShape(gtx.Ops, black, border.Op())
  365. return
  366. }
  367. func calculateHovered(
  368. bs []IPBlock,
  369. x, y float32,
  370. maxPt int,
  371. ) int {
  372. // TODO: assume IPv4, for now; support IPv6 later.
  373. ipMax := uint64(0xffff)
  374. ipX, ipY := PxToIPCoords(x, y, ipMax, maxPt)
  375. hovered, _ := findSmallestOverlappingIPBlock(bs, ipX, ipY)
  376. return hovered
  377. }
  378. /*
  379. Binary search for smallest block that contains point.
  380. Return index to smallest block or error.
  381. */
  382. func findSmallestOverlappingIPBlock(
  383. ipbs []IPBlock,
  384. ipX,
  385. ipY uint64,
  386. ) (int, error) {
  387. a := 0
  388. b := len(ipbs)
  389. if b == 0 {
  390. return -1, errors.New("empty slice")
  391. }
  392. if b == 1 && ipbs[0].ContainsPoint(ipX, ipY) {
  393. return 0, nil
  394. }
  395. if b == 1 {
  396. return -1, errors.New("no overlapping block")
  397. }
  398. cursorBlock.ipX = ipX
  399. cursorBlock.ipY = ipY
  400. // Bugs here can cause infinite loops. We avoid this by
  401. // bounding the search to its theoretical maximum
  402. // duration. TODO: why is +2 needed, here?
  403. limit := int(math.Log2(float64(len(ipbs)))) + 2
  404. for {
  405. mid := int(a + ((b - a) / 2))
  406. midLeft := ipbs[mid-1]
  407. midRight := ipbs[mid]
  408. if limit == 0 {
  409. log.Printf("midLeft: %v\n", midLeft.ToPrefix())
  410. log.Printf("midRight: %v\n", midRight.ToPrefix())
  411. log.Printf("pfx: %s, ipX: %d, ipY: %d, a: %d, b: %d, mid: %d\n", cursorBlock.ToPrefix(), ipX, ipY, a, b, mid)
  412. panic("loop limit reached")
  413. }
  414. limit--
  415. switch {
  416. case midLeft.ContainsPoint(ipX, ipY):
  417. for mid < len(ipbs) {
  418. if !ipbs[mid].ContainsPoint(ipX, ipY) {
  419. break
  420. }
  421. mid++
  422. }
  423. return mid - 1, nil
  424. case midRight.ContainsPoint(ipX, ipY):
  425. mid++
  426. for mid < len(ipbs) {
  427. if !ipbs[mid].ContainsPoint(ipX, ipY) {
  428. break
  429. }
  430. mid++
  431. }
  432. return mid - 1, nil
  433. case cursorBlock.LessThanOrEq(midLeft):
  434. b = mid - 1
  435. case midRight.LessThanOrEq(cursorBlock):
  436. a = mid + 1
  437. case cursorBlock.LessThanOrEq(midRight):
  438. return -1, errors.New("no overlapping block")
  439. default:
  440. log.Printf("midLeft: %v\n", midLeft.ToPrefix())
  441. log.Printf("midRight: %v\n", midRight.ToPrefix())
  442. log.Printf("pfx: %s, ipX: %d, ipY: %d, a: %d, b: %d, mid: %d\n", cursorBlock.ToPrefix(), ipX, ipY, a, b, mid)
  443. panic("unexpected case while finding smallest overlapping block")
  444. }
  445. }
  446. }
  447. func cmpIPBlocks(s []IPBlock) func(i, j int) bool {
  448. return func(i, j int) bool {
  449. si, sj := s[i], s[j]
  450. siPfx := si.ToPrefix()
  451. sjPfx := sj.ToPrefix()
  452. return siPfx.LessThanOrEq(sjPfx)
  453. }
  454. }
  455. func NewIPPlot(th *material.Theme) *IPPlot {
  456. ipp := IPPlot{
  457. blocktip: material.Label(th, unit.Sp(14), ""),
  458. }
  459. for _, s := range ipv4IANAReservedStrings {
  460. _, ipNet, _ := net.ParseCIDR(s)
  461. length, _ := ipNet.Mask.Size()
  462. pfx, _ := NewIPv4(ipNet.IP, length)
  463. ipb := NewIPBlock(pfx, IANAReserved)
  464. ipp.blocks = append(ipp.blocks, ipb)
  465. }
  466. sort.SliceStable(ipp.blocks, cmpIPBlocks(ipp.blocks))
  467. return &ipp
  468. }