clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / internal / generate / tiles / draw.go
draw.go 4.5K
1package tiles23import (4 "crypto/sha256"5 "encoding/hex"6 "fmt"7 "os"8 "path/filepath"9 "runtime"10 "sync"1112 "github.com/FelineStateMachine/atlas/internal/generate/tiles/basemap"13)1415// A drawn level, and what holds it honest.16//17// Most pictures in the corpus arrive as tiles from somebody's tile server, and18// the deriver's deepest level is those tiles copied through. A city has no tile19// server: what a municipal hub publishes is geometry, so the deepest level is20// drawn here, from that geometry, and the pyramid folds down from it exactly as21// any other pyramid does. Nothing below this file knows the difference.22//23// The archive still holds the rasters, because the crawl that first drew them24// wrote them down, and the plan still names their content hashes. That makes25// the capture a *witness*: every tile this deriver draws must hash to what the26// archive recorded, or the derivation fails naming the tile. It is the strongest27// statement available about a renderer -- not "these bytes look right" but28// "these bytes are the bytes a different implementation produced from the same29// vectors" -- and it is checked on every derivation rather than in a test that30// could be skipped.3132// drawing is a level's shapes, prepared once for the level they are drawn at.33type drawing struct {34 renderer *basemap.Renderer35}3637// drawingFor prepares a plan's drawing, or answers nil when the plan's picture38// is fetched rather than drawn.39func drawingFor(plan Plan, fullZoom int) (*drawing, error) {40 if plan.Drawing == nil {41 return nil, nil42 }43 // The drawing says which level it is drawn at, and the plan says which44 // level is folded down from. A disagreement would put the linework at the45 // wrong scale everywhere, silently, so it is a refusal.46 if plan.Drawing.Zoom != fullZoom {47 return nil, fmt.Errorf(48 "pyramid %s is drawn at local zoom %d but folds down from level %d",49 plan.Name, plan.Drawing.Zoom, fullZoom)50 }51 shapes := make([]basemap.Feature, 0, len(plan.Drawing.Shapes))52 for _, shape := range plan.Drawing.Shapes {53 shapes = append(shapes, basemap.Feature{54 Role: basemap.Role(shape.Role),55 Rings: shape.Rings,56 Lines: shape.Lines,57 Emphasis: shape.Emphasis,58 })59 }60 return &drawing{renderer: basemap.NewRenderer(shapes, plan.Drawing.Zoom)}, nil61}6263// level draws every tile of one level, writes the ones that are not the64// level's background, and holds each against the capture that witnessed it.65//66// Every tile is drawn, including the ones that are omitted as background: the67// point of the witness is that the whole level reproduces, and a tile skipped68// before it was drawn is a tile nothing checked.69func (d *drawing) level(70 root, name string,71 localZoom, origin int,72 tiles []Tile,73 filler string,74 mask *coverageMask,75) error {76 // Drawing is the expensive half of deriving a city, and every tile is77 // independent of every other, so the level is drawn across the machine.78 // Nothing about the result depends on the order they finish in.79 workers := min(runtime.NumCPU(), len(tiles))80 if workers < 1 {81 workers = 182 }83 var (84 next = make(chan Tile)85 mu sync.Mutex86 failure error87 group sync.WaitGroup88 )89 fail := func(err error) {90 mu.Lock()91 defer mu.Unlock()92 if failure == nil {93 failure = err94 }95 }9697 group.Add(workers)98 for range workers {99 go func() {100 defer group.Done()101 for tile := range next {102 x, y := tile.Ref.X-origin, tile.Ref.Y-origin103 body, err := basemap.EncodePNG(d.renderer.Tile(x, y))104 if err != nil {105 fail(fmt.Errorf("draw %d/%d/%d: %w", localZoom, x, y, err))106 continue107 }108 sum := sha256.Sum256(body)109 if got := hex.EncodeToString(sum[:]); got != tile.Ref.ContentHash {110 fail(fmt.Errorf(111 "drawn tile %d/%d/%d hashes to %s, the capture that witnessed it recorded %s "+112 "(the renderer and the capture must agree; see docs/generate.md ยง4.4)",113 localZoom, x, y, got, tile.Ref.ContentHash))114 continue115 }116 if filler != "" && tile.Ref.ContentHash == filler {117 continue118 }119 if err := writeTile(tilePath(root, name, localZoom, x, y, "png"), body); err != nil {120 fail(err)121 continue122 }123 mu.Lock()124 mask.mark(x, y)125 mu.Unlock()126 }127 }()128 }129 for _, tile := range tiles {130 next <- tile131 }132 close(next)133 group.Wait()134 return failure135}136137// writeTile puts one drawn tile where a copied one would have gone.138func writeTile(path string, body []byte) error {139 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {140 return err141 }142 return os.WriteFile(path, body, 0o644)143}