clone

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

main
atlas / internal / generate / tiles / tiles.go
tiles.go 9.0K
1// Package tiles reads derived tile pyramids and the stamps that say how they2// were derived.3//4// A lens is one raster pyramid picturing a world. Deriving a pyramid -- finding5// the deepest complete captured level, folding it down, choosing between6// nearest-neighbour and box reduction, recording which tiles exist -- is the7// tile pipeline's work and happens once, into a tile set on disk. Composition8// reads the result: it copies tiles into a bundle and copies the derivation's9// own account of itself into the world payload.10//11// # The derivation stamp12//13// Every pyramid carries a stamp over everything it was derived from: the plan's14// shape (which levels, which window, which encoding), the content hash of every15// source tile that went into it, and a hash of the deriving tool's own source.16// Composition folds that stamp into the bundle's stamp under the part name17//18// tiles/<pyramid>19//20// and never reads a tile to do it. That is what makes an unchanged volume cheap21// to notice: a rebuild that would write the same bytes computes the same stamp22// without decoding a single raster, and the file it would write is already23// there under a name carrying that stamp.24//25// The mechanism is one-directional on purpose. Composition trusts a pyramid's26// stamp and cannot recompute it, because the inputs -- the captured tiles the27// pyramid was folded down from -- are no longer in front of it. A pyramid whose28// stamp is empty stamps as empty, which is honest: the bundle records that29// nothing was claimed about how those tiles came to be.30//31// # What this package does not do yet32//33// Derivation itself -- frame discovery, the complete-level rule, downsampling,34// coverage bitsets, affine warp variants, offline basemap rendering -- is the35// next wave of this lane. Until it lands, a tile set is read from wherever the36// pipeline left it, and the contract this package publishes is what a deriver37// must fill in. docs/generate.md carries the derivation contract in full.38package tiles3940import (41 "encoding/json"42 "fmt"43 "io/fs"44 "os"45 "path/filepath"46 "sort"47 "strings"48)4950// IndexName is the tile set's own register, beside the pyramids it names.51const IndexName = "index.json"5253// Set is an opened tile set: a directory of derived pyramids and the register54// that says what each one is.55type Set struct {56 dir string5758 // TileSize is the edge of one tile in pixels, and Size the edge of the59 // world square every pyramid is cut from. They are properties of the whole60 // corpus rather than of one lens, which is why the manifest carries them61 // once and a bundle repeats them once.62 TileSize int63 Size int6465 byTileSet map[string][]Pyramid66 byName map[string]Pyramid67}6869// Pyramid is one derived raster pyramid as its deriver described it.70type Pyramid struct {71 // TileSet is the source path this pyramid was derived from -- the key a72 // source's lens names. Several pyramids may share one: a native picture and73 // an aligned resample of somebody else's raster picture the same ground.74 TileSet string `json:"sourcePath"`75 // Name is the directory the pyramid's tiles sit in, under the tile set76 // root. It is also what a bundle's local pyramid name is derived from.77 Name string `json:"assetPath"`78 // Stamp is the derivation stamp: what these tiles were made from and by.79 Stamp string `json:"stamp"`8081 MinZoom int `json:"minZoom"`82 MaxZoom int `json:"maxZoom"`83 FullZoom int `json:"fullZoom"`84 SourceZoom int `json:"sourceZoom"`85 // Window is where in the source tile grid this pyramid was cut from.86 Window Window `json:"grid"`87 // Formats is the file extension of each level, indexed by local zoom.88 Formats []string `json:"formats"`89 // Bounds is the part of the world square the pyramid actually draws.90 Bounds *Box `json:"bounds"`91 // Interpolate says whether the raster is smoothed when magnified: false is92 // pixel art, which is drawn nearest-neighbour.93 Interpolate bool `json:"interpolate"`94 // Background is painted behind the raster, so an omitted tile is invisible95 // rather than a hole.96 Background string `json:"background"`97 // Coverage says which tiles of a level exist, per level. A level with no98 // entry is completely covered.99 Coverage map[string]*Coverage `json:"coverage"`100 // LensName and AlignedWith mark a pyramid that is somebody else's raster101 // resampled into this world: it attaches as one more picture of whichever102 // tile set AlignedWith names.103 LensName string `json:"name"`104 AlignedWith string `json:"alignedWith"`105}106107// Window is a tile window: the zoom it is measured at and its first tile.108type Window struct {109 SourceZoom int `json:"sourceZoom"`110 FirstTile int `json:"firstTile"`111}112113// Box is a rectangle of the world square, in world pixels.114type Box struct {115 X int `json:"x"`116 Y int `json:"y"`117 Width int `json:"width"`118 Height int `json:"height"`119}120121// Coverage is which tiles of one level exist: a bounding box in tiles, and a122// row-major bitset over it, least significant bit first, base64.123type Coverage struct {124 X int `json:"x"`125 Y int `json:"y"`126 W int `json:"w"`127 H int `json:"h"`128 Bits string `json:"bits"`129}130131// Open reads a tile set's register. The path is the register file itself, so a132// caller names one artefact rather than a directory and a convention.133func Open(indexPath string) (*Set, error) {134 data, err := os.ReadFile(indexPath)135 if err != nil {136 return nil, fmt.Errorf("read tile set: %w", err)137 }138 var register struct {139 TileSize int `json:"tileSize"`140 Size int `json:"size"`141 Pyramids []Pyramid `json:"lenses"`142 }143 if err := json.Unmarshal(data, &register); err != nil {144 return nil, fmt.Errorf("decode %s: %w", indexPath, err)145 }146 if register.TileSize <= 0 || register.Size <= 0 {147 return nil, fmt.Errorf("%s declares no tile grid", indexPath)148 }149 set := &Set{150 dir: filepath.Dir(indexPath),151 TileSize: register.TileSize,152 Size: register.Size,153 byTileSet: make(map[string][]Pyramid),154 byName: make(map[string]Pyramid, len(register.Pyramids)),155 }156 for _, pyramid := range register.Pyramids {157 set.byName[pyramid.Name] = pyramid158 key := pyramid.TileSet159 if pyramid.AlignedWith != "" {160 key = pyramid.AlignedWith161 }162 set.byTileSet[key] = append(set.byTileSet[key], pyramid)163 }164 return set, nil165}166167// Dir is where the tile set's pyramids sit.168func (s *Set) Dir() string { return s.dir }169170// Native is the pyramid derived from a source's own tile set, and whether one171// exists. A world whose lens names a tile set nothing derived is not ready to172// compose.173func (s *Set) Native(tileSet string) (Pyramid, bool) {174 for _, pyramid := range s.byTileSet[tileSet] {175 if pyramid.AlignedWith == "" && pyramid.TileSet == tileSet {176 return pyramid, true177 }178 }179 return Pyramid{}, false180}181182// Aligned lists the pyramids that are somebody else's raster resampled into the183// world this tile set pictures, in register order. They attach as extra lenses184// after the native ones.185func (s *Set) Aligned(tileSet string) []Pyramid {186 var out []Pyramid187 for _, pyramid := range s.byTileSet[tileSet] {188 if pyramid.AlignedWith == tileSet {189 out = append(out, pyramid)190 }191 }192 return out193}194195// Raster is one tile inside a derived pyramid, named as a bundle entry names196// it: "<z>/<x>/<y>.<ext>". It is what composition copies; a captured Tile, which197// the deriver folds down, is a different thing entirely.198type Raster struct {199 Name string200 Path string201}202203// Tiles lists a pyramid's rasters in the order composition writes them, which204// is the order a directory walk yields them: lexical by path segment, so level205// "10" sorts before level "2". The order is part of the bundle's entry order206// and therefore part of what a reader sees when it lists an archive.207func (s *Set) Tiles(p Pyramid) ([]Raster, error) {208 root := filepath.Join(s.dir, p.Name)209 var out []Raster210 err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {211 if err != nil {212 return err213 }214 if entry.IsDir() {215 return nil216 }217 rel, err := filepath.Rel(root, path)218 if err != nil {219 return err220 }221 out = append(out, Raster{Name: filepath.ToSlash(rel), Path: path})222 return nil223 })224 if err != nil {225 return nil, fmt.Errorf("walk pyramid %s: %w", p.Name, err)226 }227 return out, nil228}229230// LocalName is what a pyramid is called inside a bundle: its tile-set name with231// the volume's own prefix taken off, so "tunic__world" travels as "world" and a232// bundle never repeats the volume it is.233func LocalName(volume, pyramid string) string {234 return strings.TrimPrefix(pyramid, volume+"__")235}236237// StampPart is the name a pyramid's derivation stamp is added to a bundle's238// stamp under. One part per pyramid, however many tiles it holds.239func StampPart(local string) string { return "tiles/" + local }240241// Names lists every pyramid in the set, sorted. It exists for the reports that242// say what a tile set holds.243func (s *Set) Names() []string {244 out := make([]string, 0, len(s.byName))245 for name := range s.byName {246 out = append(out, name)247 }248 sort.Strings(out)249 return out250}251252// Pyramid looks one up by name.253func (s *Set) Pyramid(name string) (Pyramid, bool) {254 p, ok := s.byName[name]255 return p, ok256}