219 lines
5.0 KiB
Go
219 lines
5.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"image/color"
|
|
"image/png"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"test-maze/maze"
|
|
)
|
|
|
|
const (
|
|
defaultWidth = 41
|
|
defaultHeight = 41
|
|
defaultScale = 8
|
|
)
|
|
|
|
type mazeParams struct {
|
|
Width int
|
|
Height int
|
|
Scale int
|
|
Seed uint64
|
|
Highlight bool
|
|
|
|
WallR uint8
|
|
WallG uint8
|
|
WallB uint8
|
|
|
|
PathR uint8
|
|
PathG uint8
|
|
PathB uint8
|
|
|
|
SolveR uint8
|
|
SolveG uint8
|
|
SolveB uint8
|
|
}
|
|
|
|
var pageTmpl = template.Must(template.ParseFiles("templates/index.html"))
|
|
|
|
type pageData struct {
|
|
mazeParams
|
|
MazeURL string
|
|
Query string
|
|
}
|
|
|
|
func main() {
|
|
http.HandleFunc("/", handleIndex)
|
|
http.HandleFunc("/maze.png", handleMazePNG)
|
|
|
|
addr := ":8080"
|
|
log.Printf("listening on http://localhost%s", addr)
|
|
if err := http.ListenAndServe(addr, nil); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
params := parseParams(r.URL.Query())
|
|
query := params.encode().Encode()
|
|
|
|
data := pageData{
|
|
mazeParams: params,
|
|
MazeURL: "/maze.png?" + query,
|
|
Query: query,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := pageTmpl.Execute(w, data); err != nil {
|
|
http.Error(w, "template rendering failed", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func handleMazePNG(w http.ResponseWriter, r *http.Request) {
|
|
params := parseParams(r.URL.Query())
|
|
|
|
grid, err := maze.GenerateWithSeed(params.Width, params.Height, params.Seed)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("generate maze: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
options := maze.DefaultRenderOptions()
|
|
options.Scale = params.Scale
|
|
options.HighlightPath = params.Highlight
|
|
options.WallColor = color.RGBA{R: params.WallR, G: params.WallG, B: params.WallB, A: 255}
|
|
options.PathColor = color.RGBA{R: params.PathR, G: params.PathG, B: params.PathB, A: 255}
|
|
options.SolutionColor = color.RGBA{R: params.SolveR, G: params.SolveG, B: params.SolveB, A: 255}
|
|
|
|
img, err := maze.RenderImage(grid, options)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("render maze: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.Header().Set("Content-Type", "image/png")
|
|
if err := png.Encode(w, img); err != nil {
|
|
http.Error(w, "encoding png failed", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func parseParams(q url.Values) mazeParams {
|
|
defaults := maze.DefaultRenderOptions()
|
|
defaultSeed := uint64(time.Now().UnixNano())
|
|
out := mazeParams{
|
|
Width: parseOddInt(q.Get("w"), defaultWidth, 3, 151),
|
|
Height: parseOddInt(q.Get("h"), defaultHeight, 3, 151),
|
|
Scale: parseInt(q.Get("scale"), defaultScale, 2, 24),
|
|
Seed: parseUint64(q.Get("seed"), defaultSeed),
|
|
Highlight: parseBool(q.Get("solve")),
|
|
WallR: defaults.WallColor.R,
|
|
WallG: defaults.WallColor.G,
|
|
WallB: defaults.WallColor.B,
|
|
PathR: defaults.PathColor.R,
|
|
PathG: defaults.PathColor.G,
|
|
PathB: defaults.PathColor.B,
|
|
SolveR: defaults.SolutionColor.R,
|
|
SolveG: defaults.SolutionColor.G,
|
|
SolveB: defaults.SolutionColor.B,
|
|
}
|
|
|
|
out.WallR = parseUint8(q.Get("wall_r"), out.WallR)
|
|
out.WallG = parseUint8(q.Get("wall_g"), out.WallG)
|
|
out.WallB = parseUint8(q.Get("wall_b"), out.WallB)
|
|
out.PathR = parseUint8(q.Get("path_r"), out.PathR)
|
|
out.PathG = parseUint8(q.Get("path_g"), out.PathG)
|
|
out.PathB = parseUint8(q.Get("path_b"), out.PathB)
|
|
out.SolveR = parseUint8(q.Get("solve_r"), out.SolveR)
|
|
out.SolveG = parseUint8(q.Get("solve_g"), out.SolveG)
|
|
out.SolveB = parseUint8(q.Get("solve_b"), out.SolveB)
|
|
|
|
return out
|
|
}
|
|
|
|
func (p mazeParams) encode() url.Values {
|
|
v := url.Values{}
|
|
v.Set("w", strconv.Itoa(p.Width))
|
|
v.Set("h", strconv.Itoa(p.Height))
|
|
v.Set("scale", strconv.Itoa(p.Scale))
|
|
v.Set("seed", strconv.FormatUint(p.Seed, 10))
|
|
if p.Highlight {
|
|
v.Set("solve", "1")
|
|
}
|
|
v.Set("wall_r", strconv.Itoa(int(p.WallR)))
|
|
v.Set("wall_g", strconv.Itoa(int(p.WallG)))
|
|
v.Set("wall_b", strconv.Itoa(int(p.WallB)))
|
|
v.Set("path_r", strconv.Itoa(int(p.PathR)))
|
|
v.Set("path_g", strconv.Itoa(int(p.PathG)))
|
|
v.Set("path_b", strconv.Itoa(int(p.PathB)))
|
|
v.Set("solve_r", strconv.Itoa(int(p.SolveR)))
|
|
v.Set("solve_g", strconv.Itoa(int(p.SolveG)))
|
|
v.Set("solve_b", strconv.Itoa(int(p.SolveB)))
|
|
return v
|
|
}
|
|
|
|
func parseInt(raw string, fallback, min, max int) int {
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
if n < min {
|
|
return min
|
|
}
|
|
if n > max {
|
|
return max
|
|
}
|
|
return n
|
|
}
|
|
|
|
func parseOddInt(raw string, fallback, min, max int) int {
|
|
n := parseInt(raw, fallback, min, max)
|
|
if n%2 == 0 {
|
|
if n == max {
|
|
n--
|
|
} else {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func parseUint8(raw string, fallback uint8) uint8 {
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
if n < 0 {
|
|
return 0
|
|
}
|
|
if n > 255 {
|
|
return 255
|
|
}
|
|
return uint8(n)
|
|
}
|
|
|
|
func parseUint64(raw string, fallback uint64) uint64 {
|
|
n, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return n
|
|
}
|
|
|
|
func parseBool(raw string) bool {
|
|
raw = strings.TrimSpace(strings.ToLower(raw))
|
|
switch raw {
|
|
case "1", "true", "t", "yes", "y", "on":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|