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 / install.go
install.go 3.3K
1package tiles23import (4 "encoding/json"5 "fmt"6 "os"7 "path/filepath"8 "sort"9)1011// Installing a run's worth of pyramids.12//13// The sequence is what makes a run that dies partway safe. Each pyramid is14// derived under a temporary name and arrives by one rename, so a reader of the15// tile set never sees a half-written one; pyramids the archive no longer offers16// are taken out; and the register is written last, so until the very end it17// still names the stamps of what is actually on disk. A run interrupted anywhere18// leaves a register whose stamps disagree with a plan, and the next run derives19// exactly those pyramids again.2021// Register is a tile set's index as it is written.22type Register struct {23 TileSize int `json:"tileSize"`24 Size int `json:"size"`25 Pyramids []Pyramid `json:"lenses"`26}2728// Carry decides whether a pyramid the last run left can stand: its stamp29// matches, it is still under the same name, and the directory is still there.30// Keeping one is doing nothing at all.31func (s *Set) Carry(plan Plan, stamp string) (Pyramid, bool) {32 if s == nil {33 return Pyramid{}, false34 }35 // By name rather than by tile set: an aligned variant is registered under36 // the picture it hangs on, so a lookup through the tile set it was37 // resampled from would never find one and every warp would be derived38 // again on every run.39 existing, held := s.byName[plan.Name]40 if !held || existing.TileSet != plan.TileSet {41 return Pyramid{}, false42 }43 if existing.Stamp == "" || existing.Stamp != stamp {44 return Pyramid{}, false45 }46 if _, err := os.Stat(filepath.Join(s.dir, existing.Name)); err != nil {47 // Whatever the last run left is not there to be kept.48 return Pyramid{}, false49 }50 return existing, true51}5253// Install moves the pyramids that were derived into the tile set, takes out any54// the archive no longer offers, and writes the register last.55func Install(temp, dir string, register Register, derived map[string]bool) error {56 if err := os.MkdirAll(dir, 0o755); err != nil {57 return fmt.Errorf("create tile set: %w", err)58 }59 names := make([]string, 0, len(derived))60 for name := range derived {61 names = append(names, name)62 }63 sort.Strings(names)64 for _, name := range names {65 target := filepath.Join(dir, name)66 if err := os.RemoveAll(target); err != nil {67 return fmt.Errorf("replace %s: %w", name, err)68 }69 if err := os.Rename(filepath.Join(temp, name), target); err != nil {70 return fmt.Errorf("install %s: %w", name, err)71 }72 }7374 wanted := make(map[string]bool, len(register.Pyramids))75 for _, pyramid := range register.Pyramids {76 wanted[pyramid.Name] = true77 }78 entries, err := os.ReadDir(dir)79 if err != nil {80 return err81 }82 for _, entry := range entries {83 if !entry.IsDir() || wanted[entry.Name()] {84 continue85 }86 if err := os.RemoveAll(filepath.Join(dir, entry.Name())); err != nil {87 return fmt.Errorf("remove %s: %w", entry.Name(), err)88 }89 }9091 sort.Slice(register.Pyramids, func(i, j int) bool {92 return register.Pyramids[i].TileSet < register.Pyramids[j].TileSet93 })94 data, err := json.Marshal(register)95 if err != nil {96 return err97 }98 staged := filepath.Join(temp, IndexName)99 if err := os.WriteFile(staged, append(data, '\n'), 0o644); err != nil {100 return fmt.Errorf("write %s: %w", staged, err)101 }102 return os.Rename(staged, filepath.Join(dir, IndexName))103}