cmd/releaseschedule: create

Add a program that generates the release schedule diagram used on the
release schedule wiki.

For golang/go#58820

Change-Id: I8117e28704ecad2da016bcd5b0f34cb198d0e440
Reviewed-on: https://go-review.googlesource.com/c/build/+/474675
Reviewed-by: Carlos Amedee <carlos@golang.org>
Run-TryBot: Heschi Kreinick <heschi@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Auto-Submit: Heschi Kreinick <heschi@google.com>
diff --git a/cmd/releaseschedule/main.go b/cmd/releaseschedule/main.go
new file mode 100644
index 0000000..fbf420f
--- /dev/null
+++ b/cmd/releaseschedule/main.go
@@ -0,0 +1,109 @@
+package main
+
+import (
+	"fmt"
+	"math"
+	"os"
+	"strings"
+
+	svg "github.com/ajstarks/svgo"
+)
+
+func main() {
+	if err := doMain(); err != nil {
+		panic(err)
+	}
+}
+
+func doMain() error {
+	f, err := os.OpenFile("release.svg", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	canvas := svg.New(f)
+	canvas.Start(600, 400)
+	canvas.Translate(300, 200)
+	for i, month := range strings.Split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", " ") {
+		angle := func(midx int) float64 {
+			return (float64(midx) - 3) * 2 * math.Pi / 12
+		}
+		begin, end := angle(i), angle(i+1)
+
+		// Draw a single black wedge of the calendar.
+		path := fmt.Sprintf("M 0,0 L %v,%v A 100,100 0 0 0 %v,%v L 0 0",
+			100*math.Sin(begin), 100*math.Cos(begin), 100*math.Sin(end), 100*math.Cos(end))
+		canvas.Path(path, "stroke: black; fill:none")
+
+		// Draw the text. Spin it around for readability in the second half.
+		canvas.RotateTranslate(50, 0, angle(i)*360/(2*math.Pi)+20)
+		if i < 6 {
+			canvas.Text(0, 0, month)
+		} else {
+			canvas.Group(`transform="rotate(180)"`, `transform-origin="13 -3"`)
+			canvas.Text(0, 0, month)
+			canvas.Gend()
+		}
+		canvas.Gend()
+	}
+
+	type milestone struct {
+		month, week int
+		name        string
+	}
+	milestones := []milestone{
+		{1, 1, "Planning"},
+		{1, 3, "General development"},
+		{5, 4, "Freeze"},
+		{6, 2, "First RC"},
+		{8, 2, "Release"},
+	}
+	for relIdx, relName := range []string{"Summer", "Winter"} {
+		angle := func(m milestone) float64 {
+			return (float64(m.month-1) - 3 + (float64(m.week)-0.5)/4) * 2 * math.Pi / 12
+		}
+
+		// Shift the milestones 6 months for the winter release.
+		milestones := milestones
+		for i := range milestones {
+			milestones[i].month = (milestones[i].month + 6*relIdx) % 12
+		}
+
+		frozen := false
+		for i, m := range milestones {
+			x, y := math.Cos(angle(m)), math.Sin(angle(m))
+			// Align the text away from the center of the circle.
+			textAnchor := "start"
+			if x < 0 {
+				textAnchor = "end"
+			}
+			// Color the arc depending on the freeze state.
+			if m.name == "Freeze" {
+				frozen = true
+			}
+			color := "green"
+			if frozen {
+				color = "blue"
+			}
+
+			// Center radius of the release arc.
+			arcRadius := float64(120 + 20*relIdx)
+			// Length of the line to the label. Vary a bit to avoid text overlap.
+			lineLength := float64(30 + 5*((i+1)%2))
+			// Distance from the end of the line to the text.
+			textoff := float64(10)
+
+			// Draw the arc to the next milestone.
+			if i+1 < len(milestones) {
+				nx, ny := math.Cos(angle(milestones[i+1])), math.Sin(angle(milestones[i+1]))
+				canvas.Arc(int(x*arcRadius), int(y*arcRadius), int(arcRadius), int(arcRadius), 0, false, true, int(nx*arcRadius), int(ny*arcRadius), "stroke-width:10; fill:none; stroke: "+color)
+			}
+			// Draw the line from the inner edge of the arc.
+			canvas.Line(int(x*(arcRadius-5)), int(y*(arcRadius-5)), int(x*(arcRadius+lineLength)), int(y*(arcRadius+lineLength)), "stroke:black")
+			canvas.Text(int(x*(arcRadius+lineLength+textoff)), int(y*(arcRadius+lineLength+textoff)), relName+": "+m.name, "text-anchor: "+textAnchor)
+		}
+	}
+	canvas.Gend()
+	canvas.End()
+	return f.Close()
+}
diff --git a/go.mod b/go.mod
index 06137ec..29d8367 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@
 	contrib.go.opencensus.io/exporter/stackdriver v0.13.5
 	github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf
 	github.com/NYTimes/gziphandler v1.1.1
+	github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527
 	github.com/aws/aws-sdk-go v1.30.15
 	github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625
 	github.com/creack/pty v1.1.18
@@ -73,7 +74,6 @@
 	cloud.google.com/go/monitoring v1.4.0 // indirect
 	cloud.google.com/go/trace v1.2.0 // indirect
 	github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect
-	github.com/ajstarks/svgo v0.0.0-20210923152817-c3b6e2f0c527 // indirect
 	github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
 	github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
 	github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
diff --git a/go.sum b/go.sum
index dd02309..2643b29 100644
--- a/go.sum
+++ b/go.sum
@@ -1028,8 +1028,6 @@
 golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/perf v0.0.0-20220913151710-7c6e287988f3 h1:ChUtQhjSq/uYjerTlcpLrOkD2bGMcrRuDutZVPHo3WE=
-golang.org/x/perf v0.0.0-20220913151710-7c6e287988f3/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc=
 golang.org/x/perf v0.0.0-20221222170352-3fd27c239283 h1:MTXrvYGLsaCdRXRtex0LY6RotT5TXmQ3Ru+5d1CnPIM=
 golang.org/x/perf v0.0.0-20221222170352-3fd27c239283/go.mod h1:UBKtEnL8aqnd+0JHqZ+2qoMDwtuy6cYhhKNoHLBiTQc=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=