mirror of
https://bitbucket.org/s_l_teichmann/mtsatellite
synced 2024-11-14 06:10:22 +01:00
0db9b519a6
mtseeder and mtwebmapper got an option to set the background color where no nodes are generated, yet.
248 lines
4.7 KiB
Go
248 lines
4.7 KiB
Go
// 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/ioutil"
|
|
"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.FileInfo) (min, max int) {
|
|
min, max = math.MaxInt32, math.MinInt32
|
|
for _, file := range files {
|
|
if !file.Mode().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.FileInfo) (min, max int) {
|
|
min, max = math.MaxInt32, math.MinInt32
|
|
for _, file := range files {
|
|
if !file.Mode().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 []os.FileInfo
|
|
if files, err = ioutil.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 = ioutil.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
|
|
}
|