// 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 ( "encoding/json" "image" "image/color" "image/draw" "log" "net" "net/http" "path/filepath" "strconv" "strings" "sync" "github.com/bamiaux/rez" "bytes" "bitbucket.org/s_l_teichmann/mtsatellite/common" ) // Number of check sums to keep in memory. const maxHashedTiles = 256 type baseTilesUpdates interface { BaseTilesUpdated([]xz) } type tileUpdater struct { changes map[xz]struct{} btu baseTilesUpdates mapDir string redisAddress string ips []net.IP colors *common.Colors bg color.RGBA yMin, yMax int16 workers int transparent bool cond *sync.Cond mu sync.Mutex } type xz struct { X int16 Z int16 } type xzc struct { xz canceled bool } type xzm struct { xz Mask uint16 } func (c xz) quantize() xz { return xz{X: (c.X - -1933) / 16, Z: (c.Z - -1933) / 16} } func (c xz) dequantize() xz { return xz{X: c.X*16 + -1933, Z: c.Z*16 + -1933} } func (c xz) parent() xzm { xp, xr := c.X>>1, uint16(c.X&1) zp, zr := c.Z>>1, uint16(c.Z&1) return xzm{ xz{X: xp, Z: zp}, 1 << (zr<<1 | xr)} } func newTileUpdater( mapDir, redisAddress string, ips []net.IP, colors *common.Colors, bg color.RGBA, yMin, yMax int, transparent bool, workers int, btu baseTilesUpdates) *tileUpdater { tu := tileUpdater{ btu: btu, mapDir: mapDir, redisAddress: redisAddress, ips: ips, changes: map[xz]struct{}{}, colors: colors, bg: bg, yMin: int16(yMin), yMax: int16(yMax), transparent: transparent, workers: workers} tu.cond = sync.NewCond(&tu.mu) return &tu } func (tu *tileUpdater) checkIP(r *http.Request) bool { if len(tu.ips) == 0 { return true } idx := strings.LastIndex(r.RemoteAddr, ":") if idx < 0 { log.Printf("WARN: cannot extract host from '%s'.\n", r.RemoteAddr) return false } host := strings.Trim(r.RemoteAddr[:idx], "[]") ip := net.ParseIP(host) if ip == nil { log.Printf("WARN: cannot get IP for host '%s'.\n", host) return false } for i := range tu.ips { if bytes.Compare(tu.ips[i], ip) == 0 { return true } } return false } func (tu *tileUpdater) ServeHTTP(rw http.ResponseWriter, r *http.Request) { if !tu.checkIP(r) { log.Printf("WARN: Unauthorized update request from '%s'\n", r.RemoteAddr) http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) return } var err error var newChanges []xz decoder := json.NewDecoder(r.Body) if err = decoder.Decode(&newChanges); err != nil { log.Printf("WARN: JSON document broken: %s\n", err) http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } if len(newChanges) > 0 { tu.cond.L.Lock() for _, c := range newChanges { tu.changes[c.quantize()] = struct{}{} } tu.cond.L.Unlock() tu.cond.Signal() } rw.WriteHeader(http.StatusOK) } func extractChanges(changes map[xz]struct{}) []xzc { chs := make([]xzc, len(changes)) var i int for ch := range changes { chs[i] = xzc{ch, false} i++ } return chs } func activeChanges(changes []xzc) []xz { chs := make([]xz, 0, len(changes)) for i := range changes { if !changes[i].canceled { chs = append(chs, changes[i].xz) } } return chs } func (tu *tileUpdater) doUpdates() { bth := common.NewBaseTileHash(maxHashedTiles) baseDir := filepath.Join(tu.mapDir, "8") for { tu.cond.L.Lock() for len(tu.changes) == 0 { tu.cond.Wait() } changes := extractChanges(tu.changes) tu.changes = map[xz]struct{}{} tu.cond.L.Unlock() jobs := make(chan *xzc) var done sync.WaitGroup var proto string if strings.ContainsRune(tu.redisAddress, '/') { proto = "unix" } else { proto = "tcp" } for i, n := 0, common.Min(tu.workers, len(changes)); i < n; i++ { var client *common.RedisClient var err error if client, err = common.NewRedisClient(proto, tu.redisAddress); err != nil { log.Printf("WARN: Cannot connect to redis server: %s\n", err) continue } btc := common.NewBaseTileCreator( client, tu.colors, tu.bg, tu.yMin, tu.yMax, tu.transparent, baseDir) done.Add(1) go tu.updateBaseTiles(jobs, btc, &done, bth.Update) } for i := range changes { jobs <- &changes[i] } close(jobs) done.Wait() actChs := activeChanges(changes) if len(actChs) == 0 { continue } parentJobs := make(map[xz]uint16) for i := range actChs { pxz := actChs[i].parent() parentJobs[pxz.xz] |= pxz.Mask } for level := 7; level >= 0; level-- { pJobs := make(chan xzm) for i, n := 0, common.Min(len(parentJobs), tu.workers); i < n; i++ { done.Add(1) go tu.updatePyramidTiles(level, pJobs, &done) } ppJobs := make(map[xz]uint16) for c, mask := range parentJobs { pJobs <- xzm{c, mask} pxz := c.parent() ppJobs[pxz.xz] |= pxz.Mask } close(pJobs) done.Wait() parentJobs = ppJobs } if tu.btu != nil { tu.btu.BaseTilesUpdated(actChs) } } } func (tu *tileUpdater) updatePyramidTiles( level int, jobs chan xzm, done *sync.WaitGroup) { defer done.Done() scratch := image.NewRGBA(image.Rect(0, 0, 256, 256)) resized := image.NewRGBA(image.Rect(0, 0, 128, 128)) for job := range jobs { if err := tu.updatePyramidTile(scratch, resized, level, job); err != nil { log.Printf("Updating pyramid tile failed: %s\n", err) } } } /* (0,0) (128, 0) (0, 128) (128, 128) */ var dps = [4]image.Point{ image.Pt(0, 128), image.Pt(128, 128), image.Pt(0, 0), image.Pt(128, 0), } var ofs = [4][2]int{ {0, 0}, {1, 0}, {0, 1}, {1, 1}} var windowSize = image.Pt(128, 128) func (tu *tileUpdater) updatePyramidTile(scratch, resized *image.RGBA, level int, j xzm) error { var orig image.Image origPath := filepath.Join( tu.mapDir, strconv.Itoa(level), strconv.Itoa(int(j.X)), strconv.Itoa(int(j.Z))+".png") sr := resized.Bounds() levelDir := strconv.Itoa(level + 1) for i := uint16(0); i < 4; i++ { if j.Mask&(1<