2015-07-26 16:33:29 +02:00
|
|
|
// Copyright 2014, 2015 by Sascha L. Teichmann
|
2014-09-18 07:52:37 +02:00
|
|
|
// Use of this source code is governed by the MIT license
|
|
|
|
// that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
2014-09-18 15:21:40 +02:00
|
|
|
import (
|
|
|
|
"encoding/json"
|
2014-09-21 17:30:19 +02:00
|
|
|
"image"
|
2016-04-23 16:45:33 +02:00
|
|
|
"image/color"
|
2014-09-21 17:30:19 +02:00
|
|
|
"image/draw"
|
2014-09-18 15:21:40 +02:00
|
|
|
"log"
|
2014-11-15 13:40:39 +01:00
|
|
|
"net"
|
2014-09-18 15:21:40 +02:00
|
|
|
"net/http"
|
2014-09-19 13:06:04 +02:00
|
|
|
"path/filepath"
|
2014-09-21 17:30:19 +02:00
|
|
|
"strconv"
|
2014-11-15 13:40:39 +01:00
|
|
|
"strings"
|
2014-09-19 13:06:04 +02:00
|
|
|
"sync"
|
|
|
|
|
2015-12-25 22:07:54 +01:00
|
|
|
"github.com/bamiaux/rez"
|
2014-09-21 17:30:19 +02:00
|
|
|
|
2014-11-15 13:40:39 +01:00
|
|
|
"bytes"
|
|
|
|
|
2014-10-03 12:07:53 +02:00
|
|
|
"bitbucket.org/s_l_teichmann/mtsatellite/common"
|
2014-09-19 13:06:04 +02:00
|
|
|
)
|
|
|
|
|
2016-05-10 21:02:16 +02:00
|
|
|
// Number of check sums to keep in memory.
|
|
|
|
const maxHashedTiles = 256
|
|
|
|
|
2015-03-02 13:10:30 +01:00
|
|
|
type baseTilesUpdates interface {
|
2016-05-08 22:30:00 +02:00
|
|
|
BaseTilesUpdated([]xz)
|
2015-03-02 13:10:30 +01:00
|
|
|
}
|
|
|
|
|
2014-09-18 07:52:37 +02:00
|
|
|
type tileUpdater struct {
|
2016-05-08 23:20:51 +02:00
|
|
|
changes map[xz]struct{}
|
2015-03-02 13:10:30 +01:00
|
|
|
btu baseTilesUpdates
|
2014-09-19 13:06:04 +02:00
|
|
|
mapDir string
|
|
|
|
redisAddress string
|
2014-11-15 13:40:39 +01:00
|
|
|
ips []net.IP
|
2014-09-19 13:06:04 +02:00
|
|
|
colors *common.Colors
|
2016-04-23 16:45:33 +02:00
|
|
|
bg color.RGBA
|
2015-07-27 19:03:47 +02:00
|
|
|
yMin, yMax int16
|
2014-09-19 13:06:04 +02:00
|
|
|
workers int
|
2014-10-26 18:36:47 +01:00
|
|
|
transparent bool
|
2014-09-19 13:06:04 +02:00
|
|
|
cond *sync.Cond
|
|
|
|
mu sync.Mutex
|
2014-09-18 07:52:37 +02:00
|
|
|
}
|
|
|
|
|
2014-09-18 15:21:40 +02:00
|
|
|
type xz struct {
|
|
|
|
X int16
|
|
|
|
Z int16
|
|
|
|
}
|
|
|
|
|
2016-05-08 15:13:40 +02:00
|
|
|
type xzc struct {
|
|
|
|
xz
|
|
|
|
canceled bool
|
|
|
|
}
|
|
|
|
|
2014-09-21 12:57:21 +02:00
|
|
|
type xzm struct {
|
2016-05-08 23:07:19 +02:00
|
|
|
xz
|
2014-09-21 12:57:21 +02:00
|
|
|
Mask uint16
|
|
|
|
}
|
|
|
|
|
2014-09-19 13:06:04 +02:00
|
|
|
func (c xz) quantize() xz {
|
2014-09-20 21:57:01 +02:00
|
|
|
return xz{X: (c.X - -1933) / 16, Z: (c.Z - -1933) / 16}
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
2014-09-18 07:52:37 +02:00
|
|
|
|
2014-09-21 12:57:21 +02:00
|
|
|
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{
|
2016-05-08 23:07:19 +02:00
|
|
|
xz{X: xp, Z: zp},
|
|
|
|
1 << (zr<<1 | xr)}
|
2014-09-21 17:30:19 +02:00
|
|
|
}
|
|
|
|
|
2014-10-26 18:36:47 +01:00
|
|
|
func newTileUpdater(
|
|
|
|
mapDir, redisAddress string,
|
2014-11-15 13:40:39 +01:00
|
|
|
ips []net.IP,
|
2014-10-26 18:36:47 +01:00
|
|
|
colors *common.Colors,
|
2016-04-23 16:45:33 +02:00
|
|
|
bg color.RGBA,
|
2015-07-27 19:03:47 +02:00
|
|
|
yMin, yMax int,
|
2014-10-26 18:36:47 +01:00
|
|
|
transparent bool,
|
2015-03-02 13:10:30 +01:00
|
|
|
workers int,
|
|
|
|
btu baseTilesUpdates) *tileUpdater {
|
2014-10-26 18:36:47 +01:00
|
|
|
|
2014-09-19 13:06:04 +02:00
|
|
|
tu := tileUpdater{
|
2015-03-02 13:10:30 +01:00
|
|
|
btu: btu,
|
2014-09-19 13:06:04 +02:00
|
|
|
mapDir: mapDir,
|
|
|
|
redisAddress: redisAddress,
|
2014-11-15 13:40:39 +01:00
|
|
|
ips: ips,
|
2016-05-08 23:20:51 +02:00
|
|
|
changes: map[xz]struct{}{},
|
2014-09-19 13:06:04 +02:00
|
|
|
colors: colors,
|
2016-04-23 16:45:33 +02:00
|
|
|
bg: bg,
|
2015-07-27 19:03:47 +02:00
|
|
|
yMin: int16(yMin),
|
|
|
|
yMax: int16(yMax),
|
2014-10-26 18:36:47 +01:00
|
|
|
transparent: transparent,
|
2014-09-19 13:06:04 +02:00
|
|
|
workers: workers}
|
|
|
|
tu.cond = sync.NewCond(&tu.mu)
|
|
|
|
return &tu
|
2014-09-18 07:52:37 +02:00
|
|
|
}
|
|
|
|
|
2014-11-15 13:40:39 +01:00
|
|
|
func (tu *tileUpdater) checkIP(r *http.Request) bool {
|
|
|
|
if len(tu.ips) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2016-04-06 11:44:51 +02:00
|
|
|
idx := strings.LastIndex(r.RemoteAddr, ":")
|
|
|
|
if idx < 0 {
|
2015-07-20 14:19:41 +02:00
|
|
|
log.Printf("WARN: cannot extract host from '%s'.\n", r.RemoteAddr)
|
2014-11-15 13:40:39 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-04-06 11:44:51 +02:00
|
|
|
host := strings.Trim(r.RemoteAddr[:idx], "[]")
|
|
|
|
ip := net.ParseIP(host)
|
|
|
|
if ip == nil {
|
|
|
|
log.Printf("WARN: cannot get IP for host '%s'.\n", host)
|
2014-11-15 13:40:39 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range tu.ips {
|
|
|
|
if bytes.Compare(tu.ips[i], ip) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2014-09-18 07:52:37 +02:00
|
|
|
func (tu *tileUpdater) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
2014-11-15 13:40:39 +01:00
|
|
|
if !tu.checkIP(r) {
|
2015-07-20 14:19:41 +02:00
|
|
|
log.Printf("WARN: Unauthorized update request from '%s'\n", r.RemoteAddr)
|
2014-11-15 13:40:39 +01:00
|
|
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-18 15:21:40 +02:00
|
|
|
var err error
|
2014-09-19 13:06:04 +02:00
|
|
|
var newChanges []xz
|
2014-09-18 15:21:40 +02:00
|
|
|
decoder := json.NewDecoder(r.Body)
|
2014-09-19 13:06:04 +02:00
|
|
|
if err = decoder.Decode(&newChanges); err != nil {
|
2015-07-20 14:19:41 +02:00
|
|
|
log.Printf("WARN: JSON document broken: %s\n", err)
|
2014-09-18 15:21:40 +02:00
|
|
|
http.Error(rw, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2014-09-20 14:39:51 +02:00
|
|
|
if len(newChanges) > 0 {
|
|
|
|
tu.cond.L.Lock()
|
|
|
|
for _, c := range newChanges {
|
2016-05-08 23:20:51 +02:00
|
|
|
tu.changes[c.quantize()] = struct{}{}
|
2014-09-20 14:39:51 +02:00
|
|
|
}
|
|
|
|
tu.cond.L.Unlock()
|
|
|
|
tu.cond.Signal()
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
|
|
|
|
2014-09-18 15:21:40 +02:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
2014-09-18 07:52:37 +02:00
|
|
|
}
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2016-05-08 23:20:51 +02:00
|
|
|
func extractChanges(changes map[xz]struct{}) []xzc {
|
2016-05-08 15:13:40 +02:00
|
|
|
chs := make([]xzc, len(changes))
|
|
|
|
var i int
|
|
|
|
for ch := range changes {
|
|
|
|
chs[i] = xzc{ch, false}
|
|
|
|
i++
|
|
|
|
}
|
|
|
|
return chs
|
|
|
|
}
|
|
|
|
|
2016-05-08 22:30:00 +02:00
|
|
|
func activeChanges(changes []xzc) []xz {
|
|
|
|
chs := make([]xz, 0, len(changes))
|
2016-05-08 15:13:40 +02:00
|
|
|
for i := range changes {
|
|
|
|
if !changes[i].canceled {
|
2016-05-08 22:30:00 +02:00
|
|
|
chs = append(chs, changes[i].xz)
|
2016-05-08 15:13:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return chs
|
|
|
|
}
|
|
|
|
|
2014-09-20 15:21:01 +02:00
|
|
|
func (tu *tileUpdater) doUpdates() {
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2016-05-10 21:02:16 +02:00
|
|
|
bth := common.NewBaseTileHash(maxHashedTiles)
|
2016-05-08 15:38:50 +02:00
|
|
|
|
2016-05-08 23:27:11 +02:00
|
|
|
baseDir := filepath.Join(tu.mapDir, "8")
|
|
|
|
|
2014-09-20 15:21:01 +02:00
|
|
|
for {
|
|
|
|
tu.cond.L.Lock()
|
|
|
|
for len(tu.changes) == 0 {
|
|
|
|
tu.cond.Wait()
|
|
|
|
}
|
2016-05-08 15:13:40 +02:00
|
|
|
changes := extractChanges(tu.changes)
|
2016-05-08 23:20:51 +02:00
|
|
|
tu.changes = map[xz]struct{}{}
|
2014-09-20 15:21:01 +02:00
|
|
|
tu.cond.L.Unlock()
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2016-05-08 15:13:40 +02:00
|
|
|
jobs := make(chan *xzc)
|
2014-09-20 15:21:01 +02:00
|
|
|
var done sync.WaitGroup
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2017-03-05 14:38:30 +01:00
|
|
|
var proto string
|
|
|
|
if strings.ContainsRune(tu.redisAddress, '/') {
|
|
|
|
proto = "unix"
|
|
|
|
} else {
|
|
|
|
proto = "tcp"
|
|
|
|
}
|
|
|
|
|
2015-07-26 12:32:59 +02:00
|
|
|
for i, n := 0, common.Min(tu.workers, len(changes)); i < n; i++ {
|
2014-09-20 15:21:01 +02:00
|
|
|
var client *common.RedisClient
|
|
|
|
var err error
|
2017-03-05 14:38:30 +01:00
|
|
|
if client, err = common.NewRedisClient(proto, tu.redisAddress); err != nil {
|
2015-07-20 14:19:41 +02:00
|
|
|
log.Printf("WARN: Cannot connect to redis server: %s\n", err)
|
2014-09-20 15:21:01 +02:00
|
|
|
continue
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
2015-07-27 19:03:47 +02:00
|
|
|
btc := common.NewBaseTileCreator(
|
2016-04-23 16:45:33 +02:00
|
|
|
client, tu.colors, tu.bg,
|
2015-07-27 19:03:47 +02:00
|
|
|
tu.yMin, tu.yMax,
|
2017-03-06 15:43:30 +01:00
|
|
|
tu.transparent, baseDir)
|
2014-09-20 15:21:01 +02:00
|
|
|
done.Add(1)
|
2017-03-06 15:43:30 +01:00
|
|
|
go tu.updateBaseTiles(jobs, btc, &done, bth.Update)
|
2014-09-20 15:21:01 +02:00
|
|
|
}
|
2014-09-19 13:06:04 +02:00
|
|
|
|
2016-05-08 15:13:40 +02:00
|
|
|
for i := range changes {
|
|
|
|
jobs <- &changes[i]
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
2014-09-21 12:57:21 +02:00
|
|
|
close(jobs)
|
2014-09-20 15:21:01 +02:00
|
|
|
done.Wait()
|
2014-09-21 12:57:21 +02:00
|
|
|
|
2016-05-08 15:13:40 +02:00
|
|
|
actChs := activeChanges(changes)
|
|
|
|
|
|
|
|
if len(actChs) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
parentJobs := make(map[xz]uint16)
|
2016-05-08 22:30:00 +02:00
|
|
|
for i := range actChs {
|
|
|
|
pxz := actChs[i].parent()
|
2016-05-08 23:07:19 +02:00
|
|
|
parentJobs[pxz.xz] |= pxz.Mask
|
2016-05-08 15:13:40 +02:00
|
|
|
}
|
|
|
|
|
2014-09-21 12:57:21 +02:00
|
|
|
for level := 7; level >= 0; level-- {
|
|
|
|
pJobs := make(chan xzm)
|
2015-07-26 12:32:59 +02:00
|
|
|
for i, n := 0, common.Min(len(parentJobs), tu.workers); i < n; i++ {
|
2014-09-21 12:57:21 +02:00
|
|
|
done.Add(1)
|
2016-04-23 16:45:33 +02:00
|
|
|
go tu.updatePyramidTiles(level, pJobs, &done)
|
2014-09-21 12:57:21 +02:00
|
|
|
}
|
|
|
|
ppJobs := make(map[xz]uint16)
|
|
|
|
for c, mask := range parentJobs {
|
2016-05-08 23:07:19 +02:00
|
|
|
pJobs <- xzm{c, mask}
|
2014-09-21 12:57:21 +02:00
|
|
|
pxz := c.parent()
|
2016-05-08 23:07:19 +02:00
|
|
|
ppJobs[pxz.xz] |= pxz.Mask
|
2014-09-21 12:57:21 +02:00
|
|
|
}
|
|
|
|
close(pJobs)
|
|
|
|
done.Wait()
|
|
|
|
parentJobs = ppJobs
|
|
|
|
}
|
2015-03-02 13:10:30 +01:00
|
|
|
|
|
|
|
if tu.btu != nil {
|
2016-05-08 15:13:40 +02:00
|
|
|
tu.btu.BaseTilesUpdated(actChs)
|
2015-03-02 13:10:30 +01:00
|
|
|
}
|
2014-09-21 12:57:21 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-23 16:45:33 +02:00
|
|
|
func (tu *tileUpdater) updatePyramidTiles(
|
|
|
|
level int, jobs chan xzm, done *sync.WaitGroup) {
|
|
|
|
|
2014-09-21 12:57:21 +02:00
|
|
|
defer done.Done()
|
2014-09-21 17:30:19 +02:00
|
|
|
scratch := image.NewRGBA(image.Rect(0, 0, 256, 256))
|
2015-12-25 22:07:54 +01:00
|
|
|
resized := image.NewRGBA(image.Rect(0, 0, 128, 128))
|
2014-09-21 17:30:19 +02:00
|
|
|
|
2014-09-21 12:57:21 +02:00
|
|
|
for job := range jobs {
|
2016-04-23 16:45:33 +02:00
|
|
|
if err := tu.updatePyramidTile(scratch, resized, level, job); err != nil {
|
2015-07-20 14:19:41 +02:00
|
|
|
log.Printf("Updating pyramid tile failed: %s\n", err)
|
2014-09-21 17:30:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-22 00:25:24 +02:00
|
|
|
/*
|
|
|
|
(0,0) (128, 0)
|
|
|
|
(0, 128) (128, 128)
|
|
|
|
*/
|
|
|
|
|
2014-09-21 17:30:19 +02:00
|
|
|
var dps = [4]image.Point{
|
|
|
|
image.Pt(0, 128),
|
|
|
|
image.Pt(128, 128),
|
2014-09-22 00:25:24 +02:00
|
|
|
image.Pt(0, 0),
|
|
|
|
image.Pt(128, 0),
|
|
|
|
}
|
2014-09-21 17:30:19 +02:00
|
|
|
|
|
|
|
var ofs = [4][2]int{
|
|
|
|
{0, 0},
|
|
|
|
{1, 0},
|
|
|
|
{0, 1},
|
|
|
|
{1, 1}}
|
|
|
|
|
2014-09-22 00:56:29 +02:00
|
|
|
var windowSize = image.Pt(128, 128)
|
2014-09-21 17:30:19 +02:00
|
|
|
|
2016-04-23 16:45:33 +02:00
|
|
|
func (tu *tileUpdater) updatePyramidTile(scratch, resized *image.RGBA, level int, j xzm) error {
|
2014-09-21 17:30:19 +02:00
|
|
|
|
|
|
|
var orig image.Image
|
|
|
|
|
|
|
|
origPath := filepath.Join(
|
2016-04-23 16:45:33 +02:00
|
|
|
tu.mapDir,
|
2014-09-22 00:56:29 +02:00
|
|
|
strconv.Itoa(level),
|
2016-05-08 23:07:19 +02:00
|
|
|
strconv.Itoa(int(j.X)),
|
|
|
|
strconv.Itoa(int(j.Z))+".png")
|
2014-09-21 17:30:19 +02:00
|
|
|
|
2015-12-25 22:07:54 +01:00
|
|
|
sr := resized.Bounds()
|
2016-04-23 16:45:33 +02:00
|
|
|
levelDir := strconv.Itoa(level + 1)
|
2014-09-21 17:30:19 +02:00
|
|
|
for i := uint16(0); i < 4; i++ {
|
|
|
|
if j.Mask&(1<<i) != 0 {
|
2015-07-20 14:19:41 +02:00
|
|
|
//log.Printf("level %d: modified %d\n", level, i)
|
2014-09-21 17:30:19 +02:00
|
|
|
o := ofs[i]
|
2016-05-08 23:07:19 +02:00
|
|
|
bx, bz := int(2*j.X), int(2*j.Z)
|
2014-09-21 17:30:19 +02:00
|
|
|
path := filepath.Join(
|
2016-04-23 16:45:33 +02:00
|
|
|
tu.mapDir,
|
|
|
|
levelDir,
|
2014-09-22 00:56:29 +02:00
|
|
|
strconv.Itoa(bx+o[0]),
|
2015-05-27 18:36:03 +02:00
|
|
|
strconv.Itoa(bz+o[1])+".png")
|
2016-04-23 16:45:33 +02:00
|
|
|
img := common.LoadPNG(path, tu.bg)
|
2015-12-25 22:07:54 +01:00
|
|
|
if err := rez.Convert(resized, img, common.ResizeFilter); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2014-09-21 17:30:19 +02:00
|
|
|
r := sr.Sub(sr.Min).Add(dps[i])
|
2015-12-25 22:07:54 +01:00
|
|
|
draw.Draw(scratch, r, resized, sr.Min, draw.Src)
|
2014-09-21 17:30:19 +02:00
|
|
|
} else {
|
2014-09-22 02:37:44 +02:00
|
|
|
// Load lazy
|
|
|
|
if orig == nil {
|
2016-04-23 16:45:33 +02:00
|
|
|
orig = common.LoadPNG(origPath, tu.bg)
|
2014-09-22 02:37:44 +02:00
|
|
|
}
|
2015-07-20 14:19:41 +02:00
|
|
|
//log.Printf("level %d: copied %d\n", level, i)
|
2014-09-22 00:56:29 +02:00
|
|
|
min := orig.Bounds().Min.Add(dps[i])
|
|
|
|
r := image.Rectangle{min, min.Add(windowSize)}
|
2014-09-22 02:37:44 +02:00
|
|
|
draw.Draw(scratch, r, orig, min, draw.Src)
|
2014-09-21 17:30:19 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return common.SaveAsPNGAtomic(origPath, scratch)
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
|
|
|
|
2016-04-23 16:45:33 +02:00
|
|
|
func (tu *tileUpdater) updateBaseTiles(
|
2016-05-08 15:13:40 +02:00
|
|
|
jobs chan *xzc,
|
2017-03-06 15:43:30 +01:00
|
|
|
btc *common.BaseTileCreator,
|
|
|
|
done *sync.WaitGroup,
|
|
|
|
update common.BaseTileUpdateFunc) {
|
|
|
|
|
|
|
|
type jobWriter struct {
|
|
|
|
job *xzc
|
|
|
|
wFn func() (bool, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
jWs := make(chan jobWriter)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for jw := range jWs {
|
|
|
|
updated, err := jw.wFn()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("WARN: writing tile failed: %v.\n", err)
|
|
|
|
}
|
|
|
|
if !updated {
|
|
|
|
jw.job.canceled = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
close(jWs)
|
|
|
|
btc.Close()
|
|
|
|
done.Done()
|
|
|
|
}()
|
2016-04-23 16:45:33 +02:00
|
|
|
|
2014-09-19 13:06:04 +02:00
|
|
|
for job := range jobs {
|
2014-09-21 12:57:21 +02:00
|
|
|
xz := job.dequantize()
|
2017-03-06 15:43:30 +01:00
|
|
|
if err := btc.RenderArea(xz.X-1, xz.Z-1); err != nil {
|
|
|
|
log.Printf("WARN: rendering tile failed: %v.\n", err)
|
2016-05-08 15:38:50 +02:00
|
|
|
job.canceled = true
|
2017-03-06 15:43:30 +01:00
|
|
|
continue
|
2016-05-08 15:38:50 +02:00
|
|
|
}
|
2017-03-06 15:43:30 +01:00
|
|
|
jWs <- jobWriter{job, btc.WriteFunc(int(job.X), int(job.Z), update)}
|
2014-09-19 13:06:04 +02:00
|
|
|
}
|
|
|
|
}
|