// Copyright 2014, 2015 by Sascha L. Teichmann // Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package main import ( "image" "image/color" "image/draw" "io/fs" "log" "math" "os" "path/filepath" "strconv" "strings" "sync" "bitbucket.org/s_l_teichmann/mtsatellite/common" "github.com/bamiaux/rez" ) type pyramidCreator struct { numWorkers int outDir string bg color.RGBA } func findMaxDir(files []os.DirEntry) (min, max int) { min, max = math.MaxInt32, math.MinInt32 for _, file := range files { if !file.IsDir() { continue } if x, err := strconv.Atoi(file.Name()); err == nil { if x > max { max = x } if x < min { min = x } } } return } func findMaxFile(files []os.DirEntry) (min, max int) { min, max = math.MaxInt32, math.MinInt32 for _, file := range files { if !file.Type().IsRegular() { continue } name := file.Name() name = strings.TrimSuffix(name, filepath.Ext(name)) if x, err := strconv.Atoi(name); err == nil { if x > max { max = x } if x < min { min = x } } } return } type pyramidJob struct { src [4]string dst string } func (pc *pyramidCreator) createParentLevel( oldDir string, jobs chan pyramidJob) (newDir string, err error) { oldName := filepath.Base(oldDir) var oldLevel int if oldLevel, err = strconv.Atoi(oldName); err != nil { return } if oldLevel <= 0 { return } var files []fs.DirEntry if files, err = os.ReadDir(oldDir); err != nil { return } xMin, xMax := findMaxDir(files) if xMax == math.MinInt32 { return } newLevel := oldLevel - 1 log.Printf("Generating tiles of level %d\n", newLevel) parentDir := filepath.Dir(oldDir) newDir = filepath.Join(parentDir, strconv.Itoa(newLevel)) if err = os.MkdirAll(newDir, os.ModePerm); err != nil { return } for ox, nx := xMin, xMin; ox <= xMax; ox += 2 { ox1Dir := filepath.Join(oldDir, strconv.Itoa(ox)) ox2Dir := filepath.Join(oldDir, strconv.Itoa(ox+1)) if files, err = os.ReadDir(ox1Dir); err != nil { return } zMin, zMax := findMaxFile(files) if zMax == math.MinInt32 { nx++ continue } nxDir := filepath.Join(newDir, strconv.Itoa(nx)) if err = os.MkdirAll(nxDir, os.ModePerm); err != nil { return } for oz, nz := zMin, zMin; oz <= zMax; oz += 2 { oz1 := strconv.Itoa(oz) + ".png" oz2 := strconv.Itoa(oz+1) + ".png" s1 := filepath.Join(ox1Dir, oz1) s2 := filepath.Join(ox1Dir, oz2) s3 := filepath.Join(ox2Dir, oz1) s4 := filepath.Join(ox2Dir, oz2) d := filepath.Join(nxDir, strconv.Itoa(nz)+".png") jobs <- pyramidJob{src: [4]string{s1, s2, s3, s4}, dst: d} nz++ } nx++ } return } func clip8(x int) int { switch { case x < 0: return 0 case x > 256: return 256 } return x } func clipRect(r image.Rectangle) image.Rectangle { return image.Rectangle{ Min: image.Point{X: clip8(r.Min.X), Y: clip8(r.Min.Y)}, Max: image.Point{X: clip8(r.Max.X), Y: clip8(r.Max.Y)}} } var dps = [4]image.Point{ image.Pt(0, 256), image.Pt(0, 0), image.Pt(256, 256), image.Pt(256, 0)} func (pc *pyramidCreator) fuseTile( scratch, resized *image.RGBA, conv rez.Converter, job *pyramidJob) error { for i, path := range job.src { img := common.LoadPNG(path, pc.bg) sr := clipRect(img.Bounds()) r := sr.Sub(sr.Min).Add(dps[i]) draw.Draw(scratch, r, img, sr.Min, draw.Src) } if err := conv.Convert(resized, scratch); err != nil { return err } log.Printf("Writing pyramid tile '%s'.\n", job.dst) return common.SaveAsPNG(job.dst, resized) } func (pc *pyramidCreator) fuseTiles(jobs chan pyramidJob, done *sync.WaitGroup) { defer done.Done() scratch := image.NewRGBA(image.Rect(0, 0, 512, 512)) resized := image.NewRGBA(image.Rect(0, 0, 256, 256)) cfg, err := rez.PrepareConversion(resized, scratch) if err != nil { log.Printf("WARN: cannot prepare rescaling: %s\n", err) return } conv, err := rez.NewConverter(cfg, common.ResizeFilter) if err != nil { log.Printf("WARN: Cannot create image converter: %s\n", err) return } for job := range jobs { if err := pc.fuseTile(scratch, resized, conv, &job); err != nil { log.Printf("WARN: Writing image failed: %s\n", err) } } } func (pc *pyramidCreator) create() (err error) { for oldDir := filepath.Join(pc.outDir, baseLevelDir); oldDir != ""; { if oldDir, err = pc.createLevel(oldDir); err != nil { return } } return } func (pc *pyramidCreator) createLevel(oldDir string) (string, error) { jobs := make(chan pyramidJob) var done sync.WaitGroup for i := 0; i < pc.numWorkers; i++ { done.Add(1) go pc.fuseTiles(jobs, &done) } newDir, err := pc.createParentLevel(oldDir, jobs) close(jobs) if err != nil { return newDir, err } done.Wait() return newDir, err }